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()