diff --git a/q2_alignment/_mafft.py b/q2_alignment/_mafft.py index 0932d91..67356e8 100644 --- a/q2_alignment/_mafft.py +++ b/q2_alignment/_mafft.py @@ -29,7 +29,7 @@ def run_command(cmd, output_fp, verbose=True, env=None): def _mafft(sequences_fp, alignment_fp, n_threads, parttree, addfragments, - keeplength, large): + keeplength, large, strategy, maxiterate, retree): # Save original sequence IDs since long ids (~250 chars) can be truncated # by mafft. We'll replace the IDs in the aligned sequences file output by # mafft with the originals. @@ -107,6 +107,15 @@ def _mafft(sequences_fp, alignment_fp, n_threads, parttree, addfragments, env.update({'MAFFT_TMPDIR': get_cache().get_tmp_path()}) cmd += ['--large'] + if strategy: + cmd += [("--" + strategy)] + + if maxiterate is not None: + cmd += ['--maxiterate', str(maxiterate)] + + if retree is not None: + cmd += ['--retree', str(retree)] + if alignment_fp is not None: add_flag = '--addfragments' if addfragments else '--add' cmd += [add_flag, sequences_fp, alignment_fp] @@ -141,9 +150,15 @@ def _mafft(sequences_fp, alignment_fp, n_threads, parttree, addfragments, def mafft(sequences: DNAFASTAFormat, n_threads: int = 1, parttree: bool = False, - large: bool = False) -> AlignedDNAFASTAFormat: + large: bool = False, + strategy: str | None = None, + maxiterate: int | None = None, + retree: int | None = None,) -> AlignedDNAFASTAFormat: sequences_fp = str(sequences) - return _mafft(sequences_fp, None, n_threads, parttree, False, False, large) + return _mafft( + sequences_fp, None, n_threads, parttree, False, False, large, + strategy, maxiterate, retree + ) def mafft_add(alignment: AlignedDNAFASTAFormat, @@ -152,9 +167,13 @@ def mafft_add(alignment: AlignedDNAFASTAFormat, parttree: bool = False, addfragments: bool = False, keeplength: bool = False, - large: bool = False) -> AlignedDNAFASTAFormat: + large: bool = False, + strategy: str | None = None, + maxiterate: int | None = None, + retree: int | None = None) -> AlignedDNAFASTAFormat: alignment_fp = str(alignment) sequences_fp = str(sequences) return _mafft( sequences_fp, alignment_fp, n_threads, parttree, addfragments, - keeplength, large) + keeplength, large, strategy, maxiterate, retree + ) diff --git a/q2_alignment/plugin_setup.py b/q2_alignment/plugin_setup.py index 94e0c06..66c7c0a 100644 --- a/q2_alignment/plugin_setup.py +++ b/q2_alignment/plugin_setup.py @@ -7,11 +7,45 @@ # ---------------------------------------------------------------------------- from qiime2.plugin import ( - Plugin, Float, Bool, Range, Citations, Threads) + Plugin, Float, Bool, Range, Citations, Threads, Int, Str, Choices) from q2_types.feature_data import FeatureData, Sequence, AlignedSequence import q2_alignment +mafft_params = { + "n_threads": Threads, + "parttree": Bool, + "large": Bool, + "strategy": Str % Choices({ + "auto", "nofft", "globalpair", "localpair", "genafpair", + }), + "maxiterate": Int % Range(0, None), + "retree": Int % Range(0, None), +} +mafft_param_descriptions = { + "n_threads": "The number of threads. (Use `auto` to automatically use " + "all available cores)", + "parttree": "This flag is required if the number of sequences being " + "aligned are larger than 1,000,000. Disabled by default.", + "large": "This flag is required when aligning very large datasets " + "that do not otherwise fit into memory. Temporary data is " + "then stored in files, instead of RAM. The --use-cache " + "flag specifies the storage location of the temporary files " + "created. By default, $TMP/qiime2/ is used.", + "strategy": "Specifies the multiple alignment strategy to use. " + "Exactly one strategy may be specified. Valid options " + "are: 'auto', 'nofft', 'globalpair', 'localpair', " + "and 'genafpair'. Default strategy: FFT-NS.", + 'maxiterate': 'Specifies how many iterative refinement cycles are ' + 'performed after the initial progressive alignment. ' + 'By default, no iterative refinement is performed.', + 'retree': 'Specifies the number of times the guide tree is rebuilt ' + 'during the progressive stage. Typically, tree topology ' + 'stabilizes after 2-3 iterations and higher values rarely ' + 'improves alignment quality enough to justify the extra ' + 'computation.', +} + citations = Citations.load('citations.bib', package='q2_alignment') plugin = Plugin( name='alignment', @@ -26,21 +60,14 @@ plugin.methods.register_function( function=q2_alignment.mafft, inputs={'sequences': FeatureData[Sequence]}, - parameters={'n_threads': Threads, - 'parttree': Bool, - 'large': Bool}, + parameters={ + **mafft_params, + }, outputs=[('alignment', FeatureData[AlignedSequence])], input_descriptions={'sequences': 'The sequences to be aligned.'}, parameter_descriptions={ - 'n_threads': 'The number of threads. (Use `auto` to automatically use ' - 'all available cores)', - 'parttree': 'This flag is required if the number of sequences being ' - 'aligned are larger than 1000000. Disabled by default', - 'large': 'This flag is required when aligning very large datasets ' - 'that do not otherwise fit into memory. Temporary data is ' - 'then stored in files, instead of RAM. The --use-cache ' - 'flag specifies the storage location of the temporary files ' - 'created. By default, $TMP/qiime2/ is used.'}, + **mafft_param_descriptions, + }, output_descriptions={'alignment': 'The aligned sequences.'}, name='De novo multiple sequence alignment with MAFFT', description=("Perform de novo multiple sequence alignment using MAFFT."), @@ -51,20 +78,17 @@ function=q2_alignment.mafft_add, inputs={'alignment': FeatureData[AlignedSequence], 'sequences': FeatureData[Sequence]}, - parameters={'n_threads': Threads, - 'parttree': Bool, - 'addfragments': Bool, - 'keeplength': Bool, - 'large': Bool}, + parameters={ + **mafft_params, + 'addfragments': Bool, + 'keeplength': Bool, + }, outputs=[('expanded_alignment', FeatureData[AlignedSequence])], input_descriptions={'alignment': 'The alignment to which ' 'sequences should be added.', 'sequences': 'The sequences to be added.'}, parameter_descriptions={ - 'n_threads': 'The number of threads. (Use `auto` to automatically use ' - 'all available cores)', - 'parttree': 'This flag is required if the number of sequences being ' - 'aligned are larger than 1000000. Disabled by default', + **mafft_param_descriptions, 'addfragments': 'Optimize for the addition of short sequence ' 'fragments (for example, primer or amplicon ' 'sequences). If not set, default sequence addition ' @@ -73,12 +97,7 @@ 'Any added sequence that would otherwise introduce new ' 'insertions into the alignment, will have those ' 'insertions deleted, to preserve original alignment ' - 'length.', - 'large': 'This flag is required when aligning very large datasets ' - 'that do not otherwise fit into memory. Temporary data is ' - 'then stored in files, instead of RAM. The --use-cache ' - 'flag specifies the storage location of the temporary files ' - 'created. By default, $TMP/qiime2/ is used.'}, + 'length.'}, output_descriptions={ 'expanded_alignment': 'Alignment containing the provided aligned and ' 'unaligned sequences.'}, diff --git a/q2_alignment/tests/test_mafft.py b/q2_alignment/tests/test_mafft.py index 574d812..bd087bf 100644 --- a/q2_alignment/tests/test_mafft.py +++ b/q2_alignment/tests/test_mafft.py @@ -82,6 +82,21 @@ def test_mafft_parttree_exception(self): with redirected_stdio(stderr=os.devnull): mafft(input_sequences) + @patch('q2_alignment._mafft.skbio.TabularMSA.read') + @patch('q2_alignment._mafft.run_command') + def test_mafft_parttree_flag(self, mock_run_cmd, mock_read): + input_sequences, exp = self._prepare_sequence_data() + mock_read.return_value = exp + + mafft(input_sequences, parttree=True) + + mock_run_cmd.assert_called_with( + ["mafft", "--preservecase", "--inputorder", + "--thread", "1", "--parttree", ANY], + ANY, + env=None + ) + def test_mafft_large(self): input_sequences, exp = self._prepare_sequence_data() @@ -91,6 +106,111 @@ def test_mafft_large(self): constructor=skbio.DNA) self.assertEqual(obs, exp) + @patch('q2_alignment._mafft.skbio.TabularMSA.read') + @patch('q2_alignment._mafft.run_command') + def test_mafft_globalpair_strategy(self, mock_run_cmd, mock_read): + input_sequences, exp = self._prepare_sequence_data() + mock_read.return_value = exp + + mafft(input_sequences, strategy="globalpair") + + mock_run_cmd.assert_called_with( + ["mafft", "--preservecase", "--inputorder", + "--thread", "1", "--globalpair", ANY], + ANY, + env=None + ) + + @patch('q2_alignment._mafft.skbio.TabularMSA.read') + @patch('q2_alignment._mafft.run_command') + def test_mafft_localpair_strategy(self, mock_run_cmd, mock_read): + input_sequences, exp = self._prepare_sequence_data() + mock_read.return_value = exp + + mafft(input_sequences, strategy="localpair") + + mock_run_cmd.assert_called_with( + ["mafft", "--preservecase", "--inputorder", + "--thread", "1", "--localpair", ANY], + ANY, + env=None + ) + + @patch('q2_alignment._mafft.skbio.TabularMSA.read') + @patch('q2_alignment._mafft.run_command') + def test_mafft_genafpair_strategy(self, mock_run_cmd, mock_read): + input_sequences, exp = self._prepare_sequence_data() + mock_read.return_value = exp + + mafft(input_sequences, strategy="genafpair") + + mock_run_cmd.assert_called_with( + ["mafft", "--preservecase", "--inputorder", + "--thread", "1", "--genafpair", ANY], + ANY, + env=None + ) + + @patch('q2_alignment._mafft.skbio.TabularMSA.read') + @patch('q2_alignment._mafft.run_command') + def test_mafft_maxiterate_flag(self, mock_run_cmd, mock_read): + input_sequences, exp = self._prepare_sequence_data() + mock_read.return_value = exp + + mafft(input_sequences, maxiterate=1000) + + mock_run_cmd.assert_called_with( + ["mafft", "--preservecase", "--inputorder", + "--thread", "1", "--maxiterate", "1000", ANY], + ANY, + env=None + ) + + @patch('q2_alignment._mafft.skbio.TabularMSA.read') + @patch('q2_alignment._mafft.run_command') + def test_mafft_retree_flag(self, mock_run_cmd, mock_read): + input_sequences, exp = self._prepare_sequence_data() + mock_read.return_value = exp + + mafft(input_sequences, retree=3) + + mock_run_cmd.assert_called_with( + ["mafft", "--preservecase", "--inputorder", + "--thread", "1", "--retree", "3", ANY], + ANY, + env=None + ) + + @patch('q2_alignment._mafft.skbio.TabularMSA.read') + @patch('q2_alignment._mafft.run_command') + def test_mafft_nofft_strategy(self, mock_run_cmd, mock_read): + input_sequences, exp = self._prepare_sequence_data() + mock_read.return_value = exp + + mafft(input_sequences, strategy="nofft") + + mock_run_cmd.assert_called_with( + ["mafft", "--preservecase", "--inputorder", + "--thread", "1", "--nofft", ANY], + ANY, + env=None + ) + + @patch('q2_alignment._mafft.skbio.TabularMSA.read') + @patch('q2_alignment._mafft.run_command') + def test_mafft_auto_strategy(self, mock_run_cmd, mock_read): + input_sequences, exp = self._prepare_sequence_data() + mock_read.return_value = exp + + mafft(input_sequences, strategy="auto") + + mock_run_cmd.assert_called_with( + ["mafft", "--preservecase", "--inputorder", + "--thread", "1", "--auto", ANY], + ANY, + env=None + ) + class MafftAddTests(TestPluginBase): package = 'q2_alignment.tests'