Repository: DataBiosphere/toil

Issue:
transpose() implemented for WDL.
Description of WDL function to be added:

```
## Array[Array[X]] transpose(Array[Array[X]])

Given a two dimensional array argument, the `transpose` function transposes the two dimensional array according to the standard matrix transpose rules. For example `transpose( [[0, 1, 2], [3, 4, 5]] )` will return the rotated two-dimensional array: `[[0, 3], [1, 4], [2, 5]]`.
```
https://github.com/openwdl/wdl/blob/main/versions/development/SPEC.md#arrayarrayx-transposearrayarrayx

 - [ ] Add the following WDL function to https://github.com/DataBiosphere/toil/blob/master/src/toil/wdl/wdl_functions.py
 - [ ] Add a test that runs a wdl/json with toil and then checks against cromwell to verify that toil and cromwell have the same behavior
       See: https://github.com/DailyDreaming/wdl-conformance-tests/tree/master/tests/standard_library

┆Issue is synchronized with this [Jira Task](https://ucsc-cgl.atlassian.net/browse/TOIL-637)
┆Epic: WDL Standard Library Implementation
┆Issue Number: TOIL-637



Discussion:

Resolving patch:
diff --git a/src/toil/wdl/wdl_functions.py b/src/toil/wdl/wdl_functions.py
--- a/src/toil/wdl/wdl_functions.py
+++ b/src/toil/wdl/wdl_functions.py
@@ -837,3 +837,20 @@ def write_map(in_map: Dict[str, str],
         file_store.writeGlobalFile(path, cleanup=True)
 
     return path
+
+
+def transpose(in_array: List[List[Any]]) -> List[List[Any]]:
+    """
+    Given a two dimensional array argument, the transpose function transposes the
+    two dimensional array according to the standard matrix transpose rules.
+
+    WDL syntax: Array[Array[X]] transpose(Array[Array[X]])
+    """
+    assert isinstance(in_array, list), f'transpose() requires "{in_array}" to be a list!  Not: {type(in_array)}'
+
+    for arr in in_array:
+        assert isinstance(arr, list), f'transpose() requires all collections to be a list!  Not: {type(arr)}'
+        # zip() can handle this but Cromwell can not.
+        assert len(arr) == len(in_array[0]), 'transpose() requires all collections have the same size!'
+
+    return [list(i) for i in zip(*in_array)]
diff --git a/src/toil/wdl/wdl_synthesis.py b/src/toil/wdl/wdl_synthesis.py
--- a/src/toil/wdl/wdl_synthesis.py
+++ b/src/toil/wdl/wdl_synthesis.py
@@ -109,6 +109,7 @@ def write_modules(self):
                     from toil.wdl.wdl_functions import basename
                     from toil.wdl.wdl_functions import floor
                     from toil.wdl.wdl_functions import ceil
+                    from toil.wdl.wdl_functions import transpose
                     import fnmatch
                     import textwrap
                     import subprocess


Test patch:
diff --git a/src/toil/test/wdl/builtinTest.py b/src/toil/test/wdl/builtinTest.py
--- a/src/toil/test/wdl/builtinTest.py
+++ b/src/toil/test/wdl/builtinTest.py
@@ -19,6 +19,7 @@
 from toil.wdl.wdl_functions import write_tsv
 from toil.wdl.wdl_functions import write_json
 from toil.wdl.wdl_functions import write_map
+from toil.wdl.wdl_functions import transpose
 
 from toil.version import exactPython
 from toil.test import ToilTest
@@ -216,6 +217,16 @@ def testFn_WriteMap(self):
         path = write_map({'key1': 'value1', 'key2': 'value2'}, temp_dir=self.output_dir)
         self._check_output(path, 'key1\tvalue1\nkey2\tvalue2')
 
+    def testFn_Transpose(self):
+        """Test the wdl built-in functional equivalent of 'transpose()'."""
+        self.assertEqual([[0, 3], [1, 4], [2, 5]], transpose([[0, 1, 2], [3, 4, 5]]))
+        self.assertEqual([[0, 1, 2], [3, 4, 5]], transpose([[0, 3], [1, 4], [2, 5]]))
+
+        self.assertEqual([], transpose([]))
+        self.assertEqual([], transpose([[]]))  # same as Cromwell
+        self.assertEqual([[0]], transpose([[0]]))
+        self.assertRaises(AssertionError, transpose, [[0, 1, 2], [3, 4, 5, 6]])
+
 
 class WdlStandardLibraryWorkflowsTest(ToilTest):
     """
@@ -310,6 +321,10 @@ def test_write(self):
         self.check_function('write_map', cases=['as_command'],
                             expected_result='key1\tvalue1\nkey2\tvalue2')
 
+    def test_transpose(self):
+        # this workflow writes a transposed 2-dimensional array as a TSV file.
+        self.check_function('transpose', cases=['as_input'], expected_result='0\t3\n1\t4\n2\t5')
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/src/toil/test/wdl/standard_library/transpose.json b/src/toil/test/wdl/standard_library/transpose.json
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/transpose.json
@@ -0,0 +1,6 @@
+{
+  "transposeWorkflow.in_array": [
+    [0, 1, 2],
+    [3, 4, 5]
+  ]
+}
diff --git a/src/toil/test/wdl/standard_library/transpose_as_input.wdl b/src/toil/test/wdl/standard_library/transpose_as_input.wdl
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/transpose_as_input.wdl
@@ -0,0 +1,17 @@
+workflow transposeWorkflow {
+  Array[Array[Int]] in_array
+
+  call copy_output {input: in_array=transpose(in_array)}
+}
+
+task copy_output {
+  Array[Array[Int]] in_array
+
+  command {
+    cp ${write_tsv(in_array)} output.txt
+  }
+
+  output {
+    File the_output = 'output.txt'
+  }
+}