Repository: DataBiosphere/toil

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

```
## Array[Pair[X,Y]] cross(Array[X], Array[Y])

Return the cross product of the two arrays. Array[Y][1] appears before
Array[X][1] in the output.
```
https://github.com/openwdl/wdl/blob/main/versions/development/SPEC.md#arraypairxy-crossarrayx-arrayy

 - [ ] 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

For example:

```wdl
Array[Int] xs = [ 1, 2, 3 ]
Array[String] ys = [ "a", "b" ]

Array[Pair[Int, String]] crossed = cross(xs, ys) # i.e. crossed = [ (1, "a"), (1, "b"), (2, "a"), (2, "b") ]
```

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



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
@@ -937,3 +937,15 @@ def wdl_zip(left: List[Any], right: List[Any]) -> List[WDLPair]:
 
     return list(WDLPair(left=left_val, right=right_val) for left_val, right_val in zip(left, right))
 
+
+def cross(left: List[Any], right: List[Any]) -> List[WDLPair]:
+    """
+    Return the cross product of the two arrays. Array[Y][1] appears before
+    Array[X][1] in the output.
+
+    WDL syntax: Array[Pair[X,Y]] cross(Array[X], Array[Y])
+    """
+    if not isinstance(left, list) or not isinstance(right, list):
+        raise WDLRuntimeError(f'cross() requires both inputs to be Array[]!  Not: {type(left)} and {type(right)}')
+
+    return list(WDLPair(left=left_val, right=right_val) for left_val in left for right_val in right)
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
@@ -130,6 +130,7 @@ def write_modules(self):
                     from toil.wdl.wdl_functions import transpose
                     from toil.wdl.wdl_functions import length
                     from toil.wdl.wdl_functions import wdl_zip
+                    from toil.wdl.wdl_functions import cross
                     import fnmatch
                     import textwrap
                     import subprocess
diff --git a/src/toil/wdl/wdl_types.py b/src/toil/wdl/wdl_types.py
--- a/src/toil/wdl/wdl_types.py
+++ b/src/toil/wdl/wdl_types.py
@@ -232,5 +232,10 @@ def __init__(self, left: Any, right: Any):
     def to_dict(self):
         return {'left': self.left, 'right': self.right}
 
+    def __eq__(self, other):
+        if not isinstance(other, WDLPair):
+            return False
+        return self.left == other.left and self.right == other.right
+
     def __repr__(self):
         return str(self.to_dict())


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
@@ -27,6 +27,7 @@
     write_map,
     write_tsv,
     wdl_zip,
+    cross,
     WDLPair,
     WDLRuntimeError)
 
@@ -250,19 +251,25 @@ def testFn_Zip(self):
         """Test the wdl built-in functional equivalent of 'zip()'."""
         left_array = [1, 2, 3]
         right_array = ['a', 'b', 'c']
-        zipped = wdl_zip(left_array, right_array)  # [WDLPair(1, 'a'), WDLPair(2, 'b'), WDLPair(3, 'c')]
+        zipped = wdl_zip(left_array, right_array)
+        expected_results = [WDLPair(1, 'a'), WDLPair(2, 'b'), WDLPair(3, 'c')]
 
-        self.assertEqual(3, len(zipped))
-
-        for index, pair in enumerate(zipped):
-            self.assertIsInstance(pair, WDLPair)
-            # check left and right values
-            self.assertEqual(left_array[index], pair.left)
-            self.assertEqual(right_array[index], pair.right)
+        self.assertEqual(zipped, expected_results)
 
         # input with different size should fail.
         self.assertRaises(WDLRuntimeError, wdl_zip, [1, 2, 3], ['a', 'b'])
 
+    def testFn_Cross(self):
+        """Test the wdl built-in functional equivalent of 'cross()'."""
+        left_array = [1, 2, 3]
+        right_array = ['a', 'b']
+        crossed = cross(left_array, right_array)
+        expected_results = [WDLPair(1, 'a'), WDLPair(1, 'b'),
+                            WDLPair(2, 'a'), WDLPair(2, 'b'),
+                            WDLPair(3, 'a'), WDLPair(3, 'b')]
+
+        self.assertEqual(crossed, expected_results)
+
 
 class WdlWorkflowsTest(ToilTest):
     """
@@ -463,6 +470,12 @@ def test_zip(self):
         self.check_function('zip', cases=['as_input'],
                             expected_result='[{"left":1,"right":"a"},{"left":2,"right":"b"},{"left":3,"right":"c"}]')
 
+    def test_cross(self):
+        self.check_function('cross', cases=['as_input'],
+                            expected_result='[{"left":1,"right":"a"},{"left":1,"right":"b"},'
+                                            '{"left":2,"right":"a"},{"left":2,"right":"b"},'
+                                            '{"left":3,"right":"a"},{"left":3,"right":"b"}]')
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/src/toil/test/wdl/standard_library/cross.json b/src/toil/test/wdl/standard_library/cross.json
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/cross.json
@@ -0,0 +1,11 @@
+{
+  "crossWorkflow.in_array_1": [
+    1,
+    2,
+    3
+  ],
+  "crossWorkflow.in_array_2": [
+    "a",
+    "b"
+  ]
+}
diff --git a/src/toil/test/wdl/standard_library/cross_as_input.wdl b/src/toil/test/wdl/standard_library/cross_as_input.wdl
new file mode 100644
--- /dev/null
+++ b/src/toil/test/wdl/standard_library/cross_as_input.wdl
@@ -0,0 +1,19 @@
+workflow crossWorkflow {
+  # this workflow depends on write_json()
+  Array[Int] in_array_1
+  Array[String] in_array_2
+
+  call copy_output {input: in_array=cross(in_array_1, in_array_2)}
+}
+
+task copy_output {
+  Array[Pair[Int, String]] in_array
+
+  command {
+    cp ${write_json(in_array)} output.txt
+  }
+
+  output {
+    File the_output = 'output.txt'
+  }
+}