Add backend argument to Circuit.simulate_statevector#512
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #512 +/- ##
==========================================
- Coverage 88.91% 88.84% -0.08%
==========================================
Files 49 49
Lines 7111 7127 +16
==========================================
+ Hits 6323 6332 +9
- Misses 788 795 +7 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| """ | ||
|
|
||
| statevec: Statevec | ||
| statevec: _DenseStateT_co |
There was a problem hiding this comment.
Unfortunately, mypy rejects covariant types as dataclass parameters starting with Python 3.13 (see the related thread for details):
graphix/transpiler.py:71: error: Cannot use a covariant type variable as a parameter [misc]
The CI currently passes only because it still runs on Python 3.12.
The simplest fix is to drop the covariance annotation (i.e., use _DenseStateT as the type parameter for SimulateResult instead of _DenseStateT_co). We don't rely on this covariance anywhere in the code base, and SimulateResult is mainly used for internal testing.
The remaining occurrences of _DenseStateT_co are only for annotating polymorphic methods. In that context the variance annotation has no effect, so it's better to replace them with _DenseStateT as well.
There was a problem hiding this comment.
Nice catch, thanks!
| *, | ||
| stacklevel: int = 1, | ||
| ) -> SimulateResult: | ||
| ) -> SimulateResult[_DenseStateT_co]: |
There was a problem hiding this comment.
This return type is too restrictive and forces you to use a cast in the return clause. The type variable _DenseStateT_co isn't bound by the _DenseStateBackendLiteral branch of the backend type, which means that this method is expected to return SimulateResult[_DenseStateT] without any lower-bound on _DenseStateT. That's impossible because _DenseStateT could be an empty (unconstrained) type.
We should let the function return any of the state types that _initialize_backend can produce:
| ) -> SimulateResult[_DenseStateT_co]: | |
| ) -> SimulateResult[_DenseStateT] | SimulateResult[_DenseStateT | Statevec | DensityMatrix]: |
This annotation is necessarily redundant: we have lost the information that SimulateResult is covariant, so SimulateResult[_DenseStateT] is not a subtype of SimulateResult[_DenseStateT | Statevec | DensityMatrix].
There was a problem hiding this comment.
Thanks for the explanation, but I still don't understand why this works...
-
Why is the expected type
SimulateResult[_DenseStateT] | SimulateResult[_DenseStateT | Statevec | DensityMatrix]and notSimulateResult[_DenseStateT] | SimulateResult[Statevec] | SimulateResult[DensityMatrix]? -
Why dropping the covariance fixes the issue on the lower bound of
_DenseStateT?
I would say that with covariance SimulateResult[_DenseStateT_co] | SimulateResult[_DenseStateT_co | Statevec | DensityMatrix] is equivalent to SimulateResult[_DenseStateT_co].
I understand the need for redundancy with _DenseStateT, but why does this remove the need to recast ?
There was a problem hiding this comment.
The type of _backend.state is _DenseStateT | Statevec | DensityMatrix. Therefore the call SimulateResult(_backend.state, ...) has the type SimulateResult[_DenseStateT | Statevec | DensityMatrix]. This type is a subtype of the declared result type SimulateResult[_DenseStateT] | SimulateResult[_DenseStateT | Statevec | DensityMatrix], so no cast is required.
The result type must also be a supertype of SimulateResult[_DenseStateT] in order to satisfy the overload signatures.
In general, we do not have an equation of the form F[A | B] == F[A] | F[B]. For example, a list containing elements of type A | B is not either a list of A or a list of B; it is a heterogeneous collection. The equation would happen to be true for SimulateResult, because the class only wraps one element of its parameter type, but mypy does not recognize this isomorphism. One could write an explicit conversion function that maps aSimulateResult[A | B] into SimulateResult[A] | SimulateResult[B], but that would be just a manual workaround.
I mentioned the covariance because, if SimulateResult is declared as covariant, then SimulateResult[_DenseStateT] would be a subtype of SimulateResult[_DenseStateT | Statevec | DensityMatrix]. Consequently, the union SimulateResult[_DenseStateT] | SimulateResult[_DenseStateT | Statevec | DensityMatrix] would collapse to the simpler type SimulateResult[_DenseStateT | Statevec | DensityMatrix].
|
Thanks for the review @thierry-martinez. I added your comments in 1737830 (even if I still would help some discussion to fully understand the logic). What do you think about the naming convention ("To discuss" section in the original PR text)? |
Yes, we can postpone the renaming for a latter refactoring PR. |
thierry-martinez
left a comment
There was a problem hiding this comment.
LGTM! Just one minor comment.
Co-authored-by: thierry-martinez <thierry.martinez@inria.fr>
pranav97nair
left a comment
There was a problem hiding this comment.
Looks good to me, thanks Mateo! Though I would also like to get together to make sure I understand the use of covariant types properly.
This PR adds the possibility to pass a custom
DenseStateBackendtoCircuit.simulate_statevector.Currently, backend instantiation is done internally. In a future PR, current
StatevectorBackendwill be subsumed by the jit backend which does not support symbolic calculations, and moved tographix-symbolicplugin. To continue supporting symbolic circuit simulation it will be necessary to useCircuit.simulate_statevectorwith the symbolic backend.Additionally, @pranav97nair mentioned that there is some interest in supporting circuit simulations on
DensityMatrixobjects (even if noise is not supported for circuits, and there aren't any plans to include this functionality). This PR allows so.To discuss
With the current proposal, naming is not very accurate:
graphix.transpiler.SimulateResult.statevecwould be better calledstategraphix.transpiler.Circuit.simulate_statevectorwould be better calledsimulate(orsimulate_circuitto mirrorPattern.simulate_pattern, although I find this naming redundant).However, this refactoring will have an important impact in the API, and many changes will be necessary across dependencies. Is it worth?