Skip to content

Use exact FCI traces for matrix exponentials - #279

Open
danielgaskins wants to merge 5 commits into
Qiskit:mainfrom
danielgaskins:fix/exact-expm-trace
Open

Use exact FCI traces for matrix exponentials#279
danielgaskins wants to merge 5 commits into
Qiskit:mainfrom
danielgaskins:fix/exact-expm-trace

Conversation

@danielgaskins

Copy link
Copy Markdown

Summary

The compiled FCI representation already contains every transition in the requested particle-number sector. This change uses that representation to calculate the exact sector trace and carries it through the SciPy linear operator wrapper.

Evolution and the general OrbitalRotation path now pass the exact trace to expm_multiply, including the appropriate time scaling for evolution. This removes the hard-coded zero without adding the extra work or nondeterminism of SciPy's trace estimator.

Fixes #190

Testing

  • python -m pytest -q tests/python
  • tox -e lint
  • cargo test -p qiskit-fermions-core --features pyext --lib linalg::fci
  • cargo fmt --check
  • cargo clippy -p qiskit-fermions-core --features pyext --all-targets -- -D warnings
  • cargo clippy -p qiskit-fermions-pyext --all-targets -- -D warnings

AI tool disclosure

OpenAI Codex with GPT-5 was used while preparing the implementation and tests.

@CLAassistant

CLAassistant commented Aug 15, 2026

Copy link
Copy Markdown

CLA assistant check
All committers have signed the CLA.

@mrossinek

Copy link
Copy Markdown
Member

Thank you for your contribution! I am a bit swamped this week, but will review your PR later next week! 👍

@mrossinek mrossinek left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks a lot for getting this started! This already looks like it is in good shape. I will push some changes myself to improve upon the private attirbute patching.

Comment thread python/qiskit_fermions/circuit/library/_expm_multiply.py Outdated
Comment thread python/qiskit_fermions/circuit/library/evolution.py Outdated
Comment thread python/qiskit_fermions/protocols/linear_operator.py Outdated
Comment thread tests/python/circuit/library/test_evolution_apply_unitary.py Outdated
Comment thread releasenotes/notes/exact-fci-trace-9d950da5d3abe256.yaml Outdated
mrossinek and others added 4 commits August 27, 2026 11:02
…ribute

The exact-trace change plumbed the trace to the matrix-exponential callers by
patching a private `_trace` attribute onto the SciPy `LinearOperator` returned by
`_linear_operator_`. That coupled a *public* protocol's return value to metadata
only this package's wrapper sets: `SupportsLinearOperator` promises nothing more
than a `scipy.sparse.linalg.LinearOperator`, so any other conforming operator
reaching `Evolution` raised `AttributeError` on the read. The `isinstance`
FermionOperator guard hid that today, but the comment above it records the intent
to remove the guard once `SupportsLinearOperator` folds into `OperatorTrait` --
at which point the crash goes live.

A SciPy `LinearOperator` is also simply the wrong carrier: it has nowhere to put
the value, and composing one drops any attached attribute (`scale * linop`
returns a bare `_ScaledLinearOperator`), so the metadata survived exactly one hop
by luck.

The trace does not need transporting at all. The internal
`_fci_linear_operator_` carrier already returns the native `FciLinearOperator`,
which exposes `trace` as a real, typed, documented getter next to the
matrix-vector action. Going through that carrier gives both pieces from one
object, so the private attribute, both `cast(Any, ...)` escapes and a stale
`type: ignore` all disappear, and the public wrapper goes back to returning an
unadorned operator.

This keeps the numerical win intact: still 82x faster and ~63000x more accurate
than `traceA=0.0` on a large-trace operator, measured against a dense `expm`
reference. Behaviour is unchanged -- the trace handed to SciPy is bit-identical.

The shared helper now takes the operator rather than a prebuilt operator/trace
pair, which also confines the FCI-kernel knowledge to one place instead of
leaving the fragile lookup duplicated at both call sites.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
`unittest.mock` was the only such usage in the suite -- everything else patches
via pytest's `monkeypatch` -- and one test already mixed both styles, taking
`monkeypatch` while importing `patch`.

Porting them made it clear most had stopped earning their keep. Before the
kernel refactor, every caller derived the trace itself (`_trace` off the linop,
times `scale` for `Evolution`), so asserting the `traceA` kwarg per caller was
the only way to catch a call site getting that arithmetic wrong. That derivation
now happens once, inside `_expm_multiply_fci`; the callers just forward `scale`,
and `OrbitalRotation` does not even do that. So the per-caller tests guarded
arithmetic that no longer exists where they were looking, and a third test
asserted `traceA == 8.5` when `test_fci_linear_operator.py` already asserts
`kernel.trace == 8.5` directly, with no patching at all. All three are removed.

What was never tested is the part that actually matters. `traceA` cannot change
the result -- SciPy factors out `exp(traceA / n)` and multiplies it back -- so
the sole observable effect of getting it right is how much work SciPy does. The
new preconditioning test counts matrix-vector products: 6 with the exact trace
versus 1374 without (~229x), asserted at 10x so it does not drift across SciPy
versions. That is black-box, and it is the whole point of the branch.

One kwarg assertion survives, for `trace(c * A) == c * trace(A)`: an unscaled
trace still returns the right vector, only worse-conditioned, so it has no
signature in the result and has to be observed where it is applied.

Both replacements were checked against mutations of the source -- dropping the
trace (`traceA=0.0`) and leaving it unscaled (`traceA=kernel.trace`) -- each of
which fails three tests, so these assert on behaviour rather than passing by
construction.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@mrossinek
mrossinek self-requested a review August 27, 2026 17:02
@coveralls

Copy link
Copy Markdown

Coverage Status

coverage: 98.045% (+0.02%) from 98.021% — danielgaskins:fix/exact-expm-trace into Qiskit:main

@danielgaskins

Copy link
Copy Markdown
Author

Thanks for improving this. Reading the trace directly from the FCI kernel is much cleaner than attaching it to the SciPy operator. I reviewed the updated branch, and the full check suite is passing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Relax the hard-coded traceA=0.0 in the apply-unitary evolution path

4 participants