Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-cotengra for openSUSE:Factory 
checked in at 2026-08-05 17:49:38
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-cotengra (Old)
 and      /work/SRC/openSUSE:Factory/.python-cotengra.new.16738 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-cotengra"

Wed Aug  5 17:49:38 2026 rev:6 rq:1369594 version:0.8.2

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-cotengra/python-cotengra.changes  
2026-06-18 21:38:54.163792758 +0200
+++ 
/work/SRC/openSUSE:Factory/.python-cotengra.new.16738/python-cotengra.changes   
    2026-08-05 17:50:34.386423240 +0200
@@ -1,0 +2,15 @@
+Tue Aug  4 21:11:09 UTC 2026 - Dirk Müller <[email protected]>
+
+- update to 0.8.2:
+  * Greedy and optimal path optimizers (`optimize_greedy`,
+    `optimize_optimal`, and the underlying
+    `ContractionProcessor`): size-1 indices are now ignored
+    during path finding. Such indices only ever contribute a
+    constant factor to contraction costs, but previously could
+    cause severe slowdowns - in particular a size-1 hyperedge
+    shared by many tensors created spurious fully-connected
+    structure. They are dropped up front and reintroduced when
+    the tree is rebuilt from the original inputs, so the path
+    remains valid while the search avoids the blowup.
+
+-------------------------------------------------------------------

Old:
----
  cotengra-0.8.1.tar.gz

New:
----
  cotengra-0.8.2.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-cotengra.spec ++++++
--- /var/tmp/diff_new_pack.UCdoW1/_old  2026-08-05 17:50:34.926442131 +0200
+++ /var/tmp/diff_new_pack.UCdoW1/_new  2026-08-05 17:50:34.930442271 +0200
@@ -17,7 +17,7 @@
 
 
 Name:           python-cotengra
-Version:        0.8.1
+Version:        0.8.2
 Release:        0
 Summary:        Hyper optimized contraction trees for large tensor networks 
and einsums
 License:        Apache-2.0

++++++ cotengra-0.8.1.tar.gz -> cotengra-0.8.2.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/.github/CONTRIBUTING.md 
new/cotengra-0.8.2/.github/CONTRIBUTING.md
--- old/cotengra-0.8.1/.github/CONTRIBUTING.md  1970-01-01 01:00:00.000000000 
+0100
+++ new/cotengra-0.8.2/.github/CONTRIBUTING.md  2020-02-02 01:00:00.000000000 
+0100
@@ -0,0 +1,27 @@
+# Contributing
+
+Contributions to `cotengra` in the form of
+[pull requests](https://github.com/jcmgray/cotengra/pulls) are very welcome.
+Opening an [issue](https://github.com/jcmgray/cotengra/issues) first can be
+useful for larger changes, design questions, or work that might affect public
+APIs.
+
+If this is your first time contributing on GitHub, the following guide may be
+useful:
+
+- [GitHub - Creating a pull 
request](https://help.github.com/articles/creating-a-pull-request/)
+
+Please read and follow the [`cotengra` Code of Conduct](../CODE_OF_CONDUCT.md).
+
+
+## AI Policy
+
+Please treat the [numpy AI 
policy](https://numpy.org/devdocs/dev/ai_policy.html) as a rough guide.
+
+
+## Development Guide
+
+Setup, tests, formatting, building the docs, and the full contribution
+checklist are documented in the
+[development guide](https://cotengra.readthedocs.io/en/latest/develop.html)
+(source: [`docs/develop.md`](../docs/develop.md)).
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/CODE_OF_CONDUCT.md 
new/cotengra-0.8.2/CODE_OF_CONDUCT.md
--- old/cotengra-0.8.1/CODE_OF_CONDUCT.md       1970-01-01 01:00:00.000000000 
+0100
+++ new/cotengra-0.8.2/CODE_OF_CONDUCT.md       2020-02-02 01:00:00.000000000 
+0100
@@ -0,0 +1,128 @@
+# Contributor Covenant Code of Conduct
+
+## Our Pledge
+
+We as members, contributors, and leaders pledge to make participation in our
+community a harassment-free experience for everyone, regardless of age, body
+size, visible or invisible disability, ethnicity, sex characteristics, gender
+identity and expression, level of experience, education, socio-economic status,
+nationality, personal appearance, race, religion, or sexual identity
+and orientation.
+
+We pledge to act and interact in ways that contribute to an open, welcoming,
+diverse, inclusive, and healthy community.
+
+## Our Standards
+
+Examples of behavior that contributes to creating a positive environment for 
our
+community include:
+
+* Demonstrating empathy and kindness toward other people
+* Being respectful of differing opinions, viewpoints, and experiences
+* Giving and gracefully accepting constructive feedback
+* Accepting responsibility and apologizing to those affected by our mistakes,
+  and learning from the experience
+* Focusing on what is best not just for us as individuals, but for the
+  overall community
+
+Examples of unacceptable behavior include:
+
+* The use of sexualized language or imagery, and sexual attention or
+  advances of any kind
+* Trolling, insulting or derogatory comments, or personal and political attacks
+* Public or private harassment
+* Publishing others' private information, such as a physical or email
+  address, without their explicit permission
+* Other conduct that could reasonably be considered inappropriate in a
+  professional setting
+
+## Enforcement Responsibilities
+
+Community leaders are responsible for clarifying and enforcing our standards of
+acceptable behavior and will take appropriate and fair corrective action in
+response to any behavior that they deem inappropriate, threatening, offensive,
+or harmful.
+
+Community leaders have the right and responsibility to remove, edit, or reject
+comments, commits, code, wiki edits, issues, and other contributions that are
+not aligned to this Code of Conduct, and will communicate reasons for 
moderation
+decisions when appropriate.
+
+## Scope
+
+This Code of Conduct applies within all community spaces, and also applies when
+an individual is officially representing the community in public spaces.
+Examples of representing our community include using an official e-mail 
address,
+posting via an official social media account, or acting as an appointed
+representative at an online or offline event.
+
+## Enforcement
+
+Instances of abusive, harassing, or otherwise unacceptable behavior may be
+reported to the community leaders responsible for enforcement at
[email protected].
+All complaints will be reviewed and investigated promptly and fairly.
+
+All community leaders are obligated to respect the privacy and security of the
+reporter of any incident.
+
+## Enforcement Guidelines
+
+Community leaders will follow these Community Impact Guidelines in determining
+the consequences for any action they deem in violation of this Code of Conduct:
+
+### 1. Correction
+
+**Community Impact**: Use of inappropriate language or other behavior deemed
+unprofessional or unwelcome in the community.
+
+**Consequence**: A private, written warning from community leaders, providing
+clarity around the nature of the violation and an explanation of why the
+behavior was inappropriate. A public apology may be requested.
+
+### 2. Warning
+
+**Community Impact**: A violation through a single incident or series
+of actions.
+
+**Consequence**: A warning with consequences for continued behavior. No
+interaction with the people involved, including unsolicited interaction with
+those enforcing the Code of Conduct, for a specified period of time. This
+includes avoiding interactions in community spaces as well as external channels
+like social media. Violating these terms may lead to a temporary or
+permanent ban.
+
+### 3. Temporary Ban
+
+**Community Impact**: A serious violation of community standards, including
+sustained inappropriate behavior.
+
+**Consequence**: A temporary ban from any sort of interaction or public
+communication with the community for a specified period of time. No public or
+private interaction with the people involved, including unsolicited interaction
+with those enforcing the Code of Conduct, is allowed during this period.
+Violating these terms may lead to a permanent ban.
+
+### 4. Permanent Ban
+
+**Community Impact**: Demonstrating a pattern of violation of community
+standards, including sustained inappropriate behavior,  harassment of an
+individual, or aggression toward or disparagement of classes of individuals.
+
+**Consequence**: A permanent ban from any sort of public interaction within
+the community.
+
+## Attribution
+
+This Code of Conduct is adapted from the [Contributor Covenant][homepage],
+version 2.0, available at
+https://www.contributor-covenant.org/version/2/0/code_of_conduct.html.
+
+Community Impact Guidelines were inspired by [Mozilla's code of conduct
+enforcement ladder](https://github.com/mozilla/diversity).
+
+[homepage]: https://www.contributor-covenant.org
+
+For answers to common questions about this code of conduct, see the FAQ at
+https://www.contributor-covenant.org/faq. Translations are available at
+https://www.contributor-covenant.org/translations.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/PKG-INFO new/cotengra-0.8.2/PKG-INFO
--- old/cotengra-0.8.1/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
+++ new/cotengra-0.8.2/PKG-INFO 2020-02-02 01:00:00.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.4
 Name: cotengra
-Version: 0.8.1
+Version: 0.8.2
 Summary: Hyper optimized contraction trees for large tensor networks and 
einsums.
 Project-URL: Documentation, https://cotengra.readthedocs.io/
 Project-URL: Repository, https://github.com/jcmgray/cotengra/
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/cotengra/_version.py 
new/cotengra-0.8.2/cotengra/_version.py
--- old/cotengra-0.8.1/cotengra/_version.py     2020-02-02 01:00:00.000000000 
+0100
+++ new/cotengra-0.8.2/cotengra/_version.py     2020-02-02 01:00:00.000000000 
+0100
@@ -18,7 +18,7 @@
 commit_id: str | None
 __commit_id__: str | None
 
-__version__ = version = '0.8.1'
-__version_tuple__ = version_tuple = (0, 8, 1)
+__version__ = version = '0.8.2'
+__version_tuple__ = version_tuple = (0, 8, 2)
 
 __commit_id__ = commit_id = None
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/cotengra/contract.py 
new/cotengra-0.8.2/cotengra/contract.py
--- old/cotengra-0.8.1/cotengra/contract.py     2020-02-02 01:00:00.000000000 
+0100
+++ new/cotengra-0.8.2/cotengra/contract.py     2020-02-02 01:00:00.000000000 
+0100
@@ -591,14 +591,14 @@
         A tuple of tuples, each containing the information needed to
         perform a pairwise contraction. Each tuple contains:
 
-            - ``p``: the parent node,
-            - ``l``: the left child node,
-            - ``r``: the right child node,
-            - ``tdot``: whether to use ``tensordot`` or ``einsum``,
-            - ``arg``: the argument to pass to ``tensordot`` or ``einsum``
-                i.e. ``axes`` or ``eq``,
-            - ``perm``: the permutation required after the contraction, if
-                any (only applies to tensordot).
+        - ``p``: the parent node,
+        - ``l``: the left child node,
+        - ``r``: the right child node,
+        - ``tdot``: whether to use ``tensordot`` or ``einsum``,
+        - ``arg``: the argument to pass to ``tensordot`` or ``einsum`` i.e.
+           ``axes`` or ``eq``,
+        - ``perm``: the permutation required after the contraction, if any
+          (only applies to tensordot).
 
         If both ``l`` and ``r`` are ``None``, the the operation is a single
         term simplification performed with ``einsum``.
@@ -660,14 +660,14 @@
         The sequence of contractions to perform. Each contraction should be a
         tuple containing:
 
-            - ``p``: the parent node,
-            - ``l``: the left child node,
-            - ``r``: the right child node,
-            - ``tdot``: whether to use ``tensordot`` or ``einsum``,
-            - ``arg``: the argument to pass to ``tensordot`` or ``einsum``
-                i.e. ``axes`` or ``eq``,
-            - ``perm``: the permutation required after the contraction, if
-                any (only applies to tensordot).
+        - ``p``: the parent node,
+        - ``l``: the left child node,
+        - ``r``: the right child node,
+        - ``tdot``: whether to use ``tensordot`` or ``einsum``,
+        - ``arg``: the argument to pass to ``tensordot`` or ``einsum`` i.e.
+          ``axes`` or ``eq``,
+        - ``perm``: the permutation required after the contraction, if any
+          (only applies to tensordot).
 
         e.g. built by calling ``extract_contractions(tree)``.
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/cotengra/core.py 
new/cotengra-0.8.2/cotengra/core.py
--- old/cotengra-0.8.1/cotengra/core.py 2020-02-02 01:00:00.000000000 +0100
+++ new/cotengra-0.8.2/cotengra/core.py 2020-02-02 01:00:00.000000000 +0100
@@ -1908,9 +1908,9 @@
         search : {'bfs', 'dfs', 'random'}, optional
             How to build the tree:
 
-                - 'bfs': breadth first expansion
-                - 'dfs': depth first expansion (largest nodes first)
-                - 'random': random expansion
+            - 'bfs': breadth first expansion
+            - 'dfs': depth first expansion (largest nodes first)
+            - 'random': random expansion
 
         seed : None, int or random.Random, optional
             Random number generator seed, if ``search`` is 'random'.
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/cotengra/interface.py 
new/cotengra-0.8.2/cotengra/interface.py
--- old/cotengra-0.8.1/cotengra/interface.py    2020-02-02 01:00:00.000000000 
+0100
+++ new/cotengra-0.8.2/cotengra/interface.py    2020-02-02 01:00:00.000000000 
+0100
@@ -267,10 +267,10 @@
     optimize : str, path_like, PathOptimizer, or ContractionTree
         The optimization strategy to use. This can be:
 
-            - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
-            - A ``PathOptimizer`` instance.
-            - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
-            - An explicit ``ContractionTree`` instance.
+        - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
+        - A ``PathOptimizer`` instance.
+        - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
+        - An explicit ``ContractionTree`` instance.
 
     canonicalize : bool, optional
         If ``True``, canonicalize the inputs and output so that the indices
@@ -364,14 +364,14 @@
     optimize : str, path_like, PathOptimizer, or ContractionTree
         The optimization strategy to use. This can be:
 
-            - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
-            - A ``PathOptimizer`` instance.
-            - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
-            - An explicit ``ContractionTree`` instance.
+        - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
+        - A ``PathOptimizer`` instance.
+        - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
+        - An explicit ``ContractionTree`` instance.
 
     Returns
     -------
-    tree : ContractionTree
+    ContractionTree
     """
     cls = optimize.__class__
     try:
@@ -418,10 +418,10 @@
     optimize : str, path_like, PathOptimizer, or ContractionTree
         The optimization strategy to use. This can be:
 
-            - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
-            - A ``PathOptimizer`` instance.
-            - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
-            - An explicit ``ContractionTree`` instance.
+        - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
+        - A ``PathOptimizer`` instance.
+        - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
+        - An explicit ``ContractionTree`` instance.
 
     canonicalize : bool, optional
         If ``True``, canonicalize the inputs and output so that the indices
@@ -699,10 +699,10 @@
     optimize : str, path_like, PathOptimizer, or ContractionTree
         The optimization strategy to use. This can be:
 
-            - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
-            - A ``PathOptimizer`` instance.
-            - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
-            - An explicit ``ContractionTree`` instance.
+        - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
+        - A ``PathOptimizer`` instance.
+        - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
+        - An explicit ``ContractionTree`` instance.
 
         If the optimizer provides sliced indices they will be used.
     constants : dict[int, array_like], optional
@@ -711,21 +711,19 @@
         inputs. Note this is a different format to the ``constants`` kwarg of
         :func:`einsum_expression` since it also provides the constant arrays.
     implementation : str or tuple[callable, callable], optional
-        What library to use to actually perform the contractions. Options
-        are:
+        What library to use to actually perform the contractions. Options are:
 
         - None: let cotengra choose.
         - "autoray": dispatch with autoray, using the ``tensordot`` and
-            ``einsum`` implementation of the backend.
+          ``einsum`` implementation of the backend.
         - "cotengra": use the ``tensordot`` and ``einsum`` implementation
-            of cotengra, which is based on batch matrix multiplication. This
-            is faster for some backends like numpy, and also enables
-            libraries which don't yet provide ``tensordot`` and ``einsum`` to
-            be used.
+          of cotengra, which is based on batch matrix multiplication. This
+          is faster for some backends like numpy, and also enables libraries
+          which don't yet provide ``tensordot`` and ``einsum`` to be used.
         - "cuquantum": use the cuquantum library to perform the whole
-            contraction (not just individual contractions).
+          contraction (not just individual contractions).
         - tuple[callable, callable]: manually supply the ``tensordot`` and
-            ``einsum`` implementations to use.
+          ``einsum`` implementations to use.
 
     autojit : bool, optional
         If ``True``, use :func:`autoray.autojit` to compile the contraction
@@ -828,17 +826,17 @@
     optimize : str, path_like, PathOptimizer, or ContractionTree
         The optimization strategy to use. This can be:
 
-            - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
-            - A ``PathOptimizer`` instance.
-            - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
-            - An explicit ``ContractionTree`` instance.
+        - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
+        - A ``PathOptimizer`` instance.
+        - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
+        - An explicit ``ContractionTree`` instance.
 
         If the optimizer provides sliced indices they will be used.
     strip_exponent : bool, optional
-            If ``True``, eagerly strip the exponent (in log10) from
-            intermediate tensors to control numerical problems from leaving the
-            range of the datatype. This method then returns the scaled
-            'mantissa' output array and the exponent separately.
+        If ``True``, eagerly strip the exponent (in log10) from intermediate
+        tensors to control numerical problems from leaving the range of the
+        datatype. This method then returns the scaled 'mantissa' output array
+        and the exponent separately.
     cache_expression : bool, optional
         If ``True``, cache the expression used to contract the arrays. This
         negates the overhead of pathfinding and building the expression when
@@ -850,6 +848,8 @@
     kwargs
         Passed to :func:`~cotengra.interface.array_contract_expression`.
 
+    Returns
+    -------
     array_like or (array_like, scalar)
         The result of the contraction. If ``strip_exponent`` is ``True``, the
         result is a tuple of the output array mantissae and the exponent
@@ -891,10 +891,10 @@
     optimize : str, path_like, PathOptimizer, or ContractionTree
         The optimization strategy to use. This can be:
 
-            - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
-            - A ``PathOptimizer`` instance.
-            - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
-            - An explicit ``ContractionTree`` instance.
+        - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
+        - A ``PathOptimizer`` instance.
+        - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
+        - An explicit ``ContractionTree`` instance.
 
     canonicalize : bool, optional
         If ``True``, canonicalize the inputs and output so that the indices
@@ -948,10 +948,10 @@
     optimize : str, path_like, PathOptimizer, or ContractionTree
         The optimization strategy to use. This can be:
 
-            - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
-            - A ``PathOptimizer`` instance.
-            - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
-            - An explicit ``ContractionTree`` instance.
+        - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
+        - A ``PathOptimizer`` instance.
+        - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
+        - An explicit ``ContractionTree`` instance.
 
         If the optimizer provides sliced indices they will be used.
     constants : Sequence of int, optional
@@ -966,16 +966,15 @@
 
         - None: let cotengra choose.
         - "autoray": dispatch with autoray, using the ``tensordot`` and
-            ``einsum`` implementation of the backend.
-        - "cotengra": use the ``tensordot`` and ``einsum`` implementation
-            of cotengra, which is based on batch matrix multiplication. This
-            is faster for some backends like numpy, and also enables
-            libraries which don't yet provide ``tensordot`` and ``einsum`` to
-            be used.
+          ``einsum`` implementation of the backend.
+        - "cotengra": use the ``tensordot`` and ``einsum`` implementation of
+          cotengra, which is based on batch matrix multiplication. This is
+          faster for some backends like numpy, and also enables libraries which
+          don't yet provide ``tensordot`` and ``einsum`` to be used.
         - "cuquantum": use the cuquantum library to perform the whole
-            contraction (not just individual contractions).
+          contraction (not just individual contractions).
         - tuple[callable, callable]: manually supply the ``tensordot`` and
-            ``einsum`` implementations to use.
+          ``einsum`` implementations to use.
 
     autojit : bool, optional
         If ``True``, use :func:`autoray.autojit` to compile the contraction
@@ -1058,10 +1057,10 @@
     optimize : str, path_like, PathOptimizer, or ContractionTree
         The optimization strategy to use. This can be:
 
-            - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
-            - A ``PathOptimizer`` instance.
-            - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
-            - An explicit ``ContractionTree`` instance.
+        - A string preset, e.g. ``'auto'``, ``'greedy'``, ``'optimal'``.
+        - A ``PathOptimizer`` instance.
+        - An explicit path, e.g. ``[(0, 1), (2, 3), ...]``.
+        - An explicit ``ContractionTree`` instance.
 
         If the optimizer provides sliced indices they will be used.
     strip_exponent : bool, optional
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/cotengra/pathfinders/path_basic.py 
new/cotengra-0.8.2/cotengra/pathfinders/path_basic.py
--- old/cotengra-0.8.1/cotengra/pathfinders/path_basic.py       2020-02-02 
01:00:00.000000000 +0100
+++ new/cotengra-0.8.2/cotengra/pathfinders/path_basic.py       2020-02-02 
01:00:00.000000000 +0100
@@ -349,13 +349,18 @@
         for i, term in enumerate(inputs):
             legs = []
             for ind in term:
+                d = size_dict[ind]
+                if d == 1:
+                    # we can just ignore size 1 dimensions
+                    continue
+
                 ix = self.indmap.get(ind, None)
                 if ix is None:
                     # index not processed yet
                     ix = self.indmap[ind] = c
                     self.edges[ix] = {i: None}
                     self.appearances.append(1)
-                    self.sizes.append(size_dict[ind])
+                    self.sizes.append(d)
                     c += 1
                 else:
                     # seen index already
@@ -367,7 +372,11 @@
             self.nodes[i] = tuple(legs)
 
         for ind in output:
-            self.appearances[self.indmap[ind]] += 1
+            try:
+                self.appearances[self.indmap[ind]] += 1
+            except KeyError:
+                # size 1 output indices are never registered
+                continue
 
         self.ssa = len(self.nodes)
         self.ssa_path = []
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/docs/changelog.md 
new/cotengra-0.8.2/docs/changelog.md
--- old/cotengra-0.8.1/docs/changelog.md        2020-02-02 01:00:00.000000000 
+0100
+++ new/cotengra-0.8.2/docs/changelog.md        2020-02-02 01:00:00.000000000 
+0100
@@ -1,5 +1,17 @@
 # Changelog
 
+## v0.8.2 (2026-06-22)
+
+**Enhancements**
+
+- Greedy and optimal path optimizers 
([`optimize_greedy`](cotengra.pathfinders.path_basic.optimize_greedy), 
[`optimize_optimal`](cotengra.pathfinders.path_basic.optimize_optimal), and the 
underlying `ContractionProcessor`): size-1 indices are now ignored during path 
finding. Such indices only ever contribute a constant factor to contraction 
costs, but previously could cause severe slowdowns - in particular a size-1 
hyperedge shared by many tensors created spurious fully-connected structure. 
They are dropped up front and reintroduced when the tree is rebuilt from the 
original inputs, so the path remains valid while the search avoids the blowup.
+
+**Infrastructure**
+
+- Add contributing guides, a code of conduct, and an AI contribution policy.
+- Add a changelog resolver to the docs build for rendering issue/PR 
cross-references.
+
+
 ## v0.8.1 (2026-06-08)
 
 **Bug fixes**
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/docs/conf.py 
new/cotengra-0.8.2/docs/conf.py
--- old/cotengra-0.8.1/docs/conf.py     2020-02-02 01:00:00.000000000 +0100
+++ new/cotengra-0.8.2/docs/conf.py     2020-02-02 01:00:00.000000000 +0100
@@ -150,8 +150,9 @@
 
 
 extlinks = {
-    "issue": ("https://github.com/jcmgray/cotengra/issues/%s";, "GH %s"),
-    "pull": ("https://github.com/jcmgray/cotengra/pull/%s";, "PR %s"),
+    "issue": ("https://github.com/jcmgray/cotengra/issues/%s";, "GH #%s"),
+    "pull": ("https://github.com/jcmgray/cotengra/pull/%s";, "PR #%s"),
+    "pr": ("https://github.com/jcmgray/cotengra/pull/%s";, "PR #%s"),
 }
 intersphinx_mapping = {
     "python": ("https://docs.python.org/3/";, None),
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/docs/develop.md 
new/cotengra-0.8.2/docs/develop.md
--- old/cotengra-0.8.1/docs/develop.md  1970-01-01 01:00:00.000000000 +0100
+++ new/cotengra-0.8.2/docs/develop.md  2020-02-02 01:00:00.000000000 +0100
@@ -0,0 +1,177 @@
+# Developer Notes
+
+
+## Contributing
+
+Contributions to `cotengra` are very welcome, whether they are bug reports,
+documentation fixes, examples, tests, or new features. If you are planning a
+larger change, opening an issue first is often the easiest way to check the
+approach before spending too much time on implementation.
+
+Please also read the
+[`cotengra` Code of 
Conduct](https://github.com/jcmgray/cotengra/blob/main/CODE_OF_CONDUCT.md).
+
+Things to check if new functionality is added:
+
+1. Ensure functions are unit tested. Tests that depend on optional packages
+   (`kahypar`, `optuna`, `cmaes`, `cotengrust`, ...) should
+   `pytest.importorskip("...")` so the suite still runs in the minimal
+   environment.
+2. Mark tests that require local-only resources with `@pytest.mark.localonly`.
+   The CI `test` task filters them out via `-m "not localonly"`.
+3. Ensure functions have
+   [NumPy-style 
docstrings](http://sphinxcontrib-napoleon.readthedocs.io/en/latest/example_numpy.html).
+4. Ensure code is formatted and linted with `pixi run lint`.
+5. Add to `cotengra/__init__.py` and `"__all__"` if appropriate.
+6. Add to changelog and elsewhere in docs.
+7. Experimental / unstable features go under `cotengra/experimental/`. That
+   path is explicitly omitted from coverage.
+
+
+### AI Policy
+
+Please treat the [numpy AI 
policy](https://numpy.org/devdocs/dev/ai_policy.html) as a rough guide.
+
+
+## Development Setup
+
+`cotengra` uses [pixi](https://pixi.sh) to manage development environments and
+reproducible tasks. The environments and tasks are defined in `pyproject.toml`,
+which is the source of truth for the commands below.
+
+After cloning the repository, install the pixi environments from the project
+root:
+
+```bash
+git clone https://github.com/jcmgray/cotengra.git
+cd cotengra
+pixi install
+```
+
+You can then run project tasks with `pixi run ...`. For example, to run a short
+python command inside the default test environment:
+
+```bash
+pixi run -e testpymid python -c "import cotengra; print(cotengra.__version__)"
+```
+
+
+## Running the Tests
+
+Testing `cotengra` is handled by pixi tasks. The most common commands are:
+
+```bash
+pixi run -e testpymid test    # full suite with coverage, matches CI
+```
+
+The `test` task expands to:
+
+```bash
+pytest tests/ \
+    --cov=cotengra \
+    --cov-report=xml \
+    --verbose \
+    --durations=10 \
+    -m "not localonly"
+```
+
+For a narrower check, use the `pytest` task (which runs in the `testpymid`
+environment, no marker filter) and forward arguments after `--`:
+
+```bash
+pixi run pytest -- tests/test_tree.py
+pixi run pytest -- tests/test_tree.py::test_contraction_tree_equivalency -v
+pixi run pytest -- 
"tests/test_tree.py::test_contraction_tree_equivalency[frozenset-int]" -v
+```
+
+To run the full suite in a specific environment, use `-e`:
+
+```bash
+pixi run -e testpyold test
+pixi run -e testpymid test
+pixi run -e testpynew test
+pixi run -e testjax test
+pixi run -e testtorch test
+pixi run -e testtensorflow test
+```
+
+To test the minimal dependency installation, use the `testminimal` environment
+(it omits the `full` feature, so `kahypar`, `cotengrust`, `opt_einsum`, ... are
+not installed):
+
+```bash
+pixi run -e testminimal test
+```
+
+The cross-backend contraction checks live in `tests/test_backends.py` and have
+a dedicated task:
+
+```bash
+pixi run -e testpymid test-backends
+```
+
+
+## Formatting the Code
+
+`cotengra` uses [`ruff`](https://docs.astral.sh/ruff/) to format imports and
+code style. Use the predefined pixi tasks rather than running the tools
+directly:
+
+```bash
+pixi run lint
+pixi run format
+```
+
+The `format-all` task also runs notebook cleanup with `squeaky`:
+
+```bash
+pixi run format-all
+```
+
+
+## Building the docs locally
+
+The documentation dependencies are managed by pixi. To build, clean, and serve
+the docs locally, use:
+
+```bash
+pixi run docs
+pixi run docs-clean
+pixi run docs-serve
+```
+
+The local server hosts the built docs at `http://localhost:8000/`. The
+generated HTML is in `docs/_build/html/`.
+
+On ReadTheDocs, the build is driven by `.readthedocs.yml` and uses the
+dedicated `readthedocs` pixi task.
+
+
+## Minting a release
+
+`cotengra` uses [`hatch-vcs`](https://github.com/ofek/hatch-vcs) to derive the
+version from git tags, and
+[GitHub Actions](https://github.com/jcmgray/cotengra/actions)
+to publish to [PyPI](https://pypi.org/project/cotengra/). To mint a new
+release:
+
+1. Make sure all the
+   [tests are passing on 
CI](https://github.com/jcmgray/cotengra/actions/workflows/tests.yml).
+2. `git tag` the release with the next `vX.Y.Z`.
+3. Push the tag to GitHub: `git push --tags`. The release workflow will
+   build the sdist and wheel and upload them to the
+   [PyPI **test** server](https://test.pypi.org/project/cotengra/).
+4. If the test-pypi build looks good, create a GitHub release from the
+   tag. Publishing the release triggers the same workflow to upload to
+   the [PyPI **production** server](https://pypi.org/project/cotengra/).
+5. The 
[`conda-forge/cotengra-feedstock`](https://github.com/conda-forge/cotengra-feedstock)
+   repo should automatically pick up the new PyPI release and build a
+   new [conda package](https://anaconda.org/conda-forge/cotengra); the
+   recipe should only need to be manually updated if there are, for
+   example, new dependencies.
+
+Alternate manual release steps (after tagging):
+
+1. Remove any old builds: `rm -rf dist/*`
+2. Build the sdist and wheel: `python -m build`
+3. Upload using twine: `twine upload dist/*`
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/docs/index.md 
new/cotengra-0.8.2/docs/index.md
--- old/cotengra-0.8.1/docs/index.md    2020-02-02 01:00:00.000000000 +0100
+++ new/cotengra-0.8.2/docs/index.md    2020-02-02 01:00:00.000000000 +0100
@@ -47,5 +47,6 @@
 :hidden:
 
 changelog.md
+develop.md
 GitHub Repository <https://github.com/jcmgray/cotengra>
 ```
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/docs/utils/resolve_changelog.py 
new/cotengra-0.8.2/docs/utils/resolve_changelog.py
--- old/cotengra-0.8.1/docs/utils/resolve_changelog.py  1970-01-01 
01:00:00.000000000 +0100
+++ new/cotengra-0.8.2/docs/utils/resolve_changelog.py  2020-02-02 
01:00:00.000000000 +0100
@@ -0,0 +1,129 @@
+#!/usr/bin/env python3
+"""Resolve sphinx-style cross-references in a project's changelog markdown
+to full URLs suitable for GitHub release notes.
+
+Usage:
+    python resolve_changelog.py input.md [output.md]
+
+If output.md is not given, prints to stdout.
+
+To reuse this in another project, just set ``PACKAGE`` below. The autoapi
+base URL is derived from it, assuming a ReadTheDocs site of
+``https://<package>.readthedocs.io``; override ``BASE`` if that differs.
+"""
+
+import re
+import sys
+from pathlib import Path
+
+# Name of the package to resolve references for.
+PACKAGE = "cotengra"
+
+BASE = f"https://{PACKAGE}.readthedocs.io/en/latest/autoapi";
+
+
+def find_package_root():
+    """Find the local package source directory."""
+    candidates = (
+        Path(__file__).resolve().parents[2] / PACKAGE,
+        Path.cwd() / PACKAGE,
+    )
+    for candidate in candidates:
+        if (candidate / "__init__.py").is_file():
+            return candidate
+    raise FileNotFoundError(
+        f"Could not find local {PACKAGE} package directory."
+    )
+
+
+def find_autoapi_modules(package_root=None):
+    """Find module pages that sphinx-autoapi should generate.
+
+    This intentionally scans files rather than importing modules, since this
+    script is used as a release-note helper and should have no import side
+    effects.
+    """
+    if package_root is None:
+        package_root = find_package_root()
+    else:
+        package_root = Path(package_root)
+
+    modules = set()
+    for path in package_root.rglob("*.py"):
+        rel = path.relative_to(package_root).with_suffix("")
+        parts = rel.parts
+        if parts[-1] == "__init__":
+            parts = parts[:-1]
+        modules.add(".".join((PACKAGE, *parts)))
+
+    return frozenset(modules)
+
+
+KNOWN_MODULES = find_autoapi_modules()
+
+
+def fqn_to_url(fqn):
+    """Convert a fully qualified Python name to its autoapi URL."""
+    parts = fqn.split(".")
+
+    # Find the longest known module prefix
+    best_module = None
+    for i in range(len(parts), 0, -1):
+        candidate = ".".join(parts[:i])
+        if candidate in KNOWN_MODULES:
+            best_module = candidate
+            break
+
+    if best_module is None:
+        # Fallback: assume everything except last component is the module
+        best_module = ".".join(parts[:-1]) if len(parts) > 1 else fqn
+
+    module_path = best_module.replace(".", "/")
+
+    if fqn == best_module:
+        return f"{BASE}/{module_path}/index.html"
+
+    return f"{BASE}/{module_path}/index.html#{fqn}"
+
+
+def resolve_links(text):
+    """Resolve all sphinx-style references in markdown text."""
+
+    prefix = f"{PACKAGE}."
+
+    # 1. Resolve [text](package.x.y.z) -> [text](url)
+    def _resolve_fqn(m):
+        link_text = m.group(1)
+        target = m.group(2)
+        if not target.startswith(prefix):
+            return m.group(0)
+        return f"[{link_text}]({fqn_to_url(target)})"
+
+    text = re.sub(r"\[([^\]]+)\]\(([^)]+)\)", _resolve_fqn, text)
+
+    # 2. Resolve {issue}`NUM` and {pr}`NUM` -> #NUM
+    text = re.sub(r"\{issue\}`(\d+)`", r"#\1", text)
+    text = re.sub(r"\{pr\}`(\d+)`", r"#\1", text)
+
+    return text
+
+
+def main():
+    if len(sys.argv) < 2:
+        print(__doc__.strip())
+        sys.exit(1)
+
+    with open(sys.argv[1]) as f:
+        text = f.read()
+
+    result = resolve_links(text)
+
+    if len(sys.argv) >= 3:
+        with open(sys.argv[2], "w") as f:
+            f.write(result)
+    else:
+        print(result)
+
+
+if __name__ == "__main__":
+    main()
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/cotengra-0.8.1/tests/test_paths_basic.py 
new/cotengra-0.8.2/tests/test_paths_basic.py
--- old/cotengra-0.8.1/tests/test_paths_basic.py        2020-02-02 
01:00:00.000000000 +0100
+++ new/cotengra-0.8.2/tests/test_paths_basic.py        2020-02-02 
01:00:00.000000000 +0100
@@ -1,3 +1,5 @@
+import itertools
+
 import numpy as np
 import pytest
 from numpy.testing import assert_allclose
@@ -241,3 +243,130 @@
         optimize="edgesort",
     )
     assert tree.get_path() == ((1, 2), (0, 1))
+
+
+# ---- size 1 indices ---- #
+# these are simply ignored when finding a path (they contribute a factor of 1
+# to every cost), then reintroduced when the tree is rebuilt from the original
+# inputs - so the path is valid but the search avoids any blowup from them.
+
+
+def _size_one_roundtrip(inputs, output, size_dict, which):
+    """Strip -> path -> rebuild on original inputs -> contract, vs einsum."""
+    path = {
+        "greedy": pb.optimize_greedy,
+        "optimal": pb.optimize_optimal,
+    }[which](inputs, output, size_dict)
+    tree = ctg.ContractionTree.from_path(inputs, output, size_dict, path=path)
+    eq = ctg.utils.inputs_output_to_eq(inputs, output)
+    arrays = ctg.utils.make_arrays_from_inputs(inputs, size_dict, seed=0)
+    assert_allclose(
+        tree.contract(arrays), np.einsum(eq, *arrays, optimize=True)
+    )
+    return tree
+
+
[email protected]("eq", test_case_eqs)
[email protected]("which", ["greedy", "optimal"])
[email protected]("seed", range(3))
+def test_manual_cases_with_size_one(eq, which, seed):
+    # reuse every manual eq but allow size 1 dims (d_min=1)
+    inputs, output = ctg.utils.eq_to_inputs_output(eq)
+    size_dict = ctg.utils.make_rand_size_dict_from_inputs(
+        inputs, d_min=1, d_max=3, seed=seed
+    )
+    _size_one_roundtrip(inputs, output, size_dict, which)
+
+
[email protected]("which", ["greedy", "optimal"])
+def test_all_size_one(which):
+    # closed loop, every bond size 1 -> whole thing is trivial
+    inputs = [("a", "b"), ("b", "c"), ("c", "d"), ("a", "d")]
+    output = ()
+    size_dict = dict.fromkeys("abcd", 1)
+    _size_one_roundtrip(inputs, output, size_dict, which)
+
+
[email protected]("which", ["greedy", "optimal"])
+def test_size_one_in_output(which):
+    # 'a' is size 1 AND in the output -> exercises the output-loop guard
+    inputs = [("a", "b"), ("b", "c")]
+    output = ("a", "c")
+    size_dict = {"a": 1, "b": 3, "c": 4}
+    tree = _size_one_roundtrip(inputs, output, size_dict, which)
+    arrays = ctg.utils.make_arrays_from_inputs(inputs, size_dict, seed=0)
+    assert tree.contract(arrays).shape == (1, 4)
+
+
[email protected]("which", ["greedy", "optimal"])
+def test_size_one_bond_is_outer_product(which):
+    # 'b' size-1 bond -> the contraction is really an outer product
+    inputs = [("a", "b"), ("b", "c")]
+    output = ("a", "c")
+    size_dict = {"a": 3, "b": 1, "c": 4}
+    _size_one_roundtrip(inputs, output, size_dict, which)
+
+
[email protected]("which", ["greedy", "optimal"])
+def test_size_one_hyperedge(which):
+    # 'h' size-1 shared by all n terms: previously a fully-connected blowup
+    # for optimal (no max_neighbors guard), now stripped entirely
+    n = 8
+    letters = [ctg.utils.get_symbol(i) for i in range(n)]
+    inputs = [("h", x) for x in letters]
+    output = tuple(letters)
+    size_dict = {x: 2 for x in letters}
+    size_dict["h"] = 1
+    _size_one_roundtrip(inputs, output, size_dict, which)
+
+
[email protected]("which", ["greedy", "optimal"])
+def test_scalar_after_stripping(which):
+    # one term collapses to a scalar once its size-1 dims are dropped
+    inputs = [("a", "b"), ("p", "q"), ("a", "b")]
+    output = ()
+    size_dict = {"a": 3, "b": 2, "p": 1, "q": 1}
+    _size_one_roundtrip(inputs, output, size_dict, which)
+
+
+def test_processor_strips_all_size_one():
+    inputs = [("a", "b"), ("b", "c")]
+    output = ("a",)
+    size_dict = dict.fromkeys("abc", 1)
+    cp = pb.ContractionProcessor(inputs, output, size_dict)
+    # no edges/indices registered at all...
+    assert cp.edges == {}
+    assert cp.indmap == {}
+    assert cp.sizes == []
+    # ...but the nodes (and thus path positions) are preserved
+    assert len(cp.nodes) == 2
+    assert all(legs == () for legs in cp.nodes.values())
+
+
+def test_processor_keeps_only_large_indices():
+    inputs = [("a", "b"), ("b", "c")]
+    # 'a' size 1 and in output -> must not KeyError, must not be registered
+    output = ("a",)
+    size_dict = {"a": 1, "b": 5, "c": 1}
+    cp = pb.ContractionProcessor(inputs, output, size_dict)
+    assert set(cp.indmap) == {"b"}
+    assert cp.sizes == [5]
+
+
+def test_processor_no_size_one_blowup():
+    # the motivating case: dense all-size-1 graph -> empty edge set, so optimal
+    # enumerates no contraction candidates and the build completes instantly
+    n = 20
+    inputs = [[] for _ in range(n)]
+    size_dict = {}
+    for k, (i, j) in enumerate(itertools.combinations(range(n), 2)):
+        ix = ctg.utils.get_symbol(k)
+        size_dict[ix] = 1
+        inputs[i].append(ix)
+        inputs[j].append(ix)
+    inputs = [tuple(t) for t in inputs]
+    cp = pb.ContractionProcessor(inputs, (), size_dict)
+    assert cp.edges == {}
+    path = pb.optimize_optimal(inputs, (), size_dict)
+    tree = ctg.ContractionTree.from_path(inputs, (), size_dict, path=path)
+    assert tree.is_complete()

Reply via email to