NVIDIA / NVIDIA/cuda-quantum

OperatorSum.evaluate() adds a spurious zero-coefficient identity term for multi-term operators

Open Beginner friendly
#5,348 0 comments 0 reactions 0 assignees View on GitHub

Nobody has claimed this yet.

stale-notified
Dominant language
C++
Stars
1.1k
Forks
455
Avg merge
1d 22h
Merged PRs (30d)
165

Description

Required prerequisites
  • Consult the security policy. Not a security report.
  • Read the documentation; this isn't addressed there.
  • Searched the issue tracker; found nothing on this.
  • PR attached with a failing test.
Describe the bug

OperatorSum.evaluate() (any of SpinOperator, BosonOperator, FermionOperator,
MatrixOperator) grows an extra, spurious zero-coefficient identity term every time it is
called on an operator with more than one term.

_sum_transformation (python/cudaq/operators/manipulation.py:154-188) seeds its accumulator
with arithmetics.evaluate(ScalarOperator.const(0)) and then adds every term to it, including
the first: evaluated = arithmetics.evaluate(ScalarOperator.const(0)); ...; evaluated = arithmetics.add(evaluated, evaluated_term). For the default evaluation arithmetics used by
.evaluate(), add is just op1 + op2, so the very first term is combined as 0 + term1.
The underlying C++ operator classes turn scalar + operator into an explicit zero-coefficient
identity term (degrees == []) instead of simplifying it away, so the result always carries
one more term than the input, and that extra term acts on no degrees of freedom at all.

This method is not just a display convenience - python/cudaq/dynamics/evolution.py:303 and
python/cudaq/dynamics/evolution.py:608 both call op.evaluate(**step_parameters) on every
user-supplied observable at every time step of cudaq.evolve(), so this fires on real
observables in the dynamics/evolve path, not just when a user calls .evaluate() directly.

Steps to reproduce the bug
from cudaq.operators import spin

s = spin.x(0) + spin.z(1)
print(s.term_count)            # 2
ev = s.evaluate()
print(ev.term_count)           # 3 - one term too many
print(ev)                      # (0+0i) + (1+0i) * X0 + (1+0i) * Z1
for term in ev:
    print(term.degrees)        # [], [0], [1] - the first term acts on nothing

Observed on the CUDA-Q 0.15.1 wheel (aca5853a7) and confirmed byte-identical on current
main for this file (git log aca5853a7..main -- python/cudaq/operators is empty):

2
3
(0+0i) + (1+0i) * X0 + (1+0i) * Z1
[]
[0]
[1]
Expected behavior

s.evaluate().term_count == s.term_count, and no term in the evaluated result should have an
empty degrees list. Numerically the extra term contributes exactly 0, so quantities computed
by simplification-tolerant consumers (e.g. cudaq.observe) come out correct regardless - but
any code that assumes every term of an evaluated operator acts on at least one degree of
freedom (e.g. term.degrees[0]) will hit an IndexError on the spurious term, and anything
iterating term_count terms gets one it never asked for.

Is this a regression? If it is, put the last known working version (or commit) here.

Not a regression - _sum_transformation and .evaluate() were introduced together in
da31e1b7a (#2817, "Migrating the Python operators to be C++ bindings", May 2025) and the
accumulator has never been touched since (only a copyright-year bump). No existing test checks
term_count or per-term degrees after .evaluate(); the existing coverage in
python/tests/operator/test_matrix_op.py::test_evaluation only compares to_matrix() output,
which is insensitive to an extra zero-coefficient term.

Environment
  • CUDA-Q version: 0.15.1 (aca5853a7)
  • Python version: 3.12.3
  • Operating system: Ubuntu 24.04.4 LTS
Suggestions

Seed the accumulation with the first evaluated term instead of arithmetics.evaluate(ScalarOperator.const(0)),
falling back to that only when the operator has no terms at all. PR attached.

Contributor guide

Open the contributing guide

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

Research direction

Start in python/cudaq/operators/manipulation.py:154-188, then review python/tests/operator/test_matrix_op.py::test_evaluation and the evaluation calls in python/cudaq/dynamics/evolution.py:303 and :608. Verify that multi-term SpinOperator, BosonOperator, FermionOperator, and MatrixOperator evaluations preserve term_count and contain no term with empty degrees; the existing matrix comparison should continue to pass.

Written by the indexing model from the issue text.

Assessment

Tech stack
cpp, python
Domain
quantum-computing
Issue type
Bug
Difficulty
2/5
Estimated time
1-3 hours
Activity status
Active
Clarity
Clearly specified
Newbie friendliness
85/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.