Repository: dgasmith/opt_einsum
Pull request #229 (2024-05-06 23:56:09)

handle empty contraction list in PathInfo
## Description
This simply handles the case when `PathInfo` is constructed with an empty path (e.g. for the no-op contraction `eq="->"`) with some defaults rather than raising `ZeroDivisionError`. See https://github.com/jcmgray/quimb/issues/231.

## Status
- [x] Ready to go

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/dgasmith/opt_einsum/pull/229?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+Smith) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 89.32%. Comparing base [(`1a984b7`)](https://app.codecov.io/gh/dgasmith/opt_einsum/commit/1a984b7b75f3e532e7129f6aa13f7ddc3da66e10?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+Smith) to head [(`fda68c3`)](https://app.codecov.io/gh/dgasmith/opt_einsum/pull/229?dropdown=coverage&src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Daniel+Smith).
> Report is 2 commits behind head on master.

<details><summary>Additional details and impacted files</summary>


</details>

Patch:
diff --git a/opt_einsum/contract.py b/opt_einsum/contract.py
index 600f537..1b38c04 100644
--- a/opt_einsum/contract.py
+++ b/opt_einsum/contract.py
@@ -50,13 +50,13 @@ class PathInfo:
         self.scale_list = scale_list
         self.naive_cost = Decimal(naive_cost)
         self.opt_cost = Decimal(opt_cost)
-        self.speedup = self.naive_cost / self.opt_cost
+        self.speedup = self.naive_cost / max(self.opt_cost, Decimal(1))
         self.size_list = size_list
         self.size_dict = size_dict
 
         self.shapes = [tuple(size_dict[k] for k in ks) for ks in input_subscripts.split(",")]
         self.eq = "{}->{}".format(input_subscripts, output_subscript)
-        self.largest_intermediate = Decimal(max(size_list))
+        self.largest_intermediate = Decimal(max(size_list, default=1))
 
     def __repr__(self) -> str:
         # Return the path along with a nice string representation
@@ -65,7 +65,7 @@ class PathInfo:
         path_print = [
             "  Complete contraction:  {}\n".format(self.eq),
             "         Naive scaling:  {}\n".format(len(self.indices)),
-            "     Optimized scaling:  {}\n".format(max(self.scale_list)),
+            "     Optimized scaling:  {}\n".format(max(self.scale_list, default=0)),
             "      Naive FLOP count:  {:.3e}\n".format(self.naive_cost),
             "  Optimized FLOP count:  {:.3e}\n".format(self.opt_cost),
             "   Theoretical speedup:  {:.3e}\n".format(self.speedup),


Test patch:
diff --git a/opt_einsum/tests/test_edge_cases.py b/opt_einsum/tests/test_edge_cases.py
index 6d213fb..7851b23 100644
--- a/opt_einsum/tests/test_edge_cases.py
+++ b/opt_einsum/tests/test_edge_cases.py
@@ -5,7 +5,7 @@ Tets a series of opt_einsum contraction paths to ensure the results are the same
 import numpy as np
 import pytest
 
-from opt_einsum import contract, contract_expression
+from opt_einsum import contract, contract_path, contract_expression
 
 
 def test_contract_expression_checks():
@@ -53,7 +53,6 @@ def test_contract_expression_checks():
 
 
 def test_broadcasting_contraction():
-
     a = np.random.rand(1, 5, 4)
     b = np.random.rand(4, 6)
     c = np.random.rand(5, 6)
@@ -73,7 +72,6 @@ def test_broadcasting_contraction():
 
 
 def test_broadcasting_contraction2():
-
     a = np.random.rand(1, 1, 5, 4)
     b = np.random.rand(4, 6)
     c = np.random.rand(5, 6)
@@ -93,7 +91,6 @@ def test_broadcasting_contraction2():
 
 
 def test_broadcasting_contraction3():
-
     a = np.random.rand(1, 5, 4)
     b = np.random.rand(4, 1, 6)
     c = np.random.rand(5, 6)
@@ -106,7 +103,6 @@ def test_broadcasting_contraction3():
 
 
 def test_broadcasting_contraction4():
-
     a = np.arange(64).reshape(2, 4, 8)
     ein = contract("obk,ijk->ioj", a, a, optimize=False)
     opt = contract("obk,ijk->ioj", a, a, optimize=True)
@@ -115,7 +111,6 @@ def test_broadcasting_contraction4():
 
 
 def test_can_blas_on_healed_broadcast_dimensions():
-
     expr = contract_expression("ab,bc,bd->acd", (5, 4), (1, 5), (4, 20))
     # first contraction involves broadcasting
     assert expr.contraction_list[0][2] == "bc,ab->bca"
@@ -123,3 +118,13 @@ def test_can_blas_on_healed_broadcast_dimensions():
     # but then is healed GEMM is usable
     assert expr.contraction_list[1][2] == "bca,bd->acd"
     assert expr.contraction_list[1][-1] == "GEMM"
+
+
+def test_pathinfo_for_empty_contraction():
+    eq = "->"
+    arrays = (1.0,)
+    path = []
+    _, info = contract_path(eq, *arrays, optimize=path)
+    # some info is built lazily, so check repr
+    assert repr(info)
+    assert info.largest_intermediate == 1