Repository navigation
v0.2.0
The circuit, operator and parameter types no longer wrap Qiskit. They are built
on a columnar instruction store and SymPy, and Qiskit is now an optional
extra like every other backend. This is a breaking change; see
Migrating to the framework-independent layer
below.
Added
-
One interface for generic and native types. Every backend's circuit class
is aQuantumCircuitBaseand every backend's operator class is a
QuantumOperatorBase, so a circuit can be built gate-by-gate directly in a
native type and gives the same answer as the generic one:from qc_executor.qulacs import QulacsCircuit circuit = QulacsCircuit(3) # was: convert from a QuantumCircuit only circuit.h(0) circuit.crz(1, 2, 2 * x[0])
The native operators consequently gained the whole operator algebra --
compose,adjoint,transpose,conjugate,simplify,append,
apply_layout,group_commuting,assign_parameters,paulis,coeffs,
parameters,fingerprintand content equality -- none of which they had. -
Derivatives over several observables on every backend. Previously only
Qiskit supported it; PennyLane and Qulacs refused explicitly and Pauli
propagation raised aTypeErrorfrom its converter.executor.expectation_value_derivatives(circuit, [obs_a, obs_b], "x", x=[0.6])
-
ObservableBatch, with a backend subclass per plugin, holding several native
observables to evaluate against one circuit. PennyLane measures them in one
QNode and differentiates once; Qulacs builds one operator list per observable. -
Mid-circuit measurement, qubit reset and classical conditioning:
measure(qubits, clbits),measure_all(),reset(qubits)and a
with circuit.if_(clbits, value):block. Supported on Qiskit and PennyLane;
Qulacs and Pauli propagation raiseNotImplementedError. -
QuantumCircuit.irexposes the instruction store, andCircuitIR.fingerprint()
gives a stable content digest used for result caching. -
A framework-independent gate-lowering pass (
qc_executor.base.decompose)
replacesqiskit.transpile(basis_gates=...). Backends declare what they can
execute viasupported_opcodes(), and everything else is rewritten into it.
Pauli propagation consequently gainedcrx,cry,crz,rzx,ecr,
iswap,ccx,cswap,uand others it used to reject outright. -
qiskitextra, holdingqiskitandpackaging.
Changed
- Minimum supported Qiskit version is now 1.2.0, up from 1.0.0. Qiskit
1.0/1.1'sStatevectorEstimator/StatevectorSamplerreturn results with a
different shape for batched parameter sets, which the parameter-batching
support added above relies on. - Pauli labels put qubit 0 leftmost on every backend. See the migration
notes; this changes Qiskit-backend results. ParametersandParameterare SymPy-backed rather than
qiskit.ParameterVector/ParameterVectorElement. Arithmetic such as
2 * x[0],p[0] * x[0]andsin(x[0])produces plain SymPy expressions.Executor.create("qiskit")now requires theqiskitextra.- Backends are imported lazily.
import qc_executorpulls in no quantum
framework;qc_executor.<backend>resolves on first access and isNonewhen
the extra is not installed. QuantumOperator.composeis pure. It previously mutated the left operand.PauliPropagationOperator.apply_layoutaccepts aSequence[int], matching
QuantumOperatorBase. It previously took only aDict[int, int], so a caller
written against the base class failed on it. Mappings are still accepted.- A missing optional backend is no longer a
UserWarning. TheValueErrorfrom
Executor.createstill names the extra to install. assign_parametersis pure. It bound in place and returnedself, which
made the bound circuit and the original the same object.- Native circuit and operator constructors take the base signature. Use
from_quantum_circuit/from_quantum_operatorto convert:
QulacsCircuit(qc)becomesQulacsCircuit.from_quantum_circuit(qc). circuit.iris the circuit as written, on every class. Two backends
previously exposed the lowered store; lowering now happens on the way to the
native representation.- Circuit equality requires the same type. A
QulacsCircuitand a
QuantumCircuitholding identical instructions are no longer equal. - The
hashproperty is gone from every native circuit and operator (it was an
inton some andbyteson others);fingerprint()and__hash__replace it. PennyLaneOperatorandQulacsOperatorno longer accept a list of
operators. Pass a list to the executor as before, or use the backend's
ObservableBatchsubclass directly.
Removed
dillandmapomaticdependencies, neither of which was imported anywhere.qc_executor.pauli_propagation.utils.qiskit_converterand
.utils.operator_converter; that backend no longer converts through Qiskit.
Fixed
- Backends disagreed on Pauli-label endianness.
QuantumOperator(["ZI"], [1.0])
measured qubit 1 on Qiskit but qubit 0 on PennyLane, Qulacs and Pauli
propagation, so the same code returned different numbers depending on the
backend. The cross-backend parity test missed it because it excluded Qiskit. - PennyLane discarded numeric observable coefficients. An observable with no
free parameters was evaluated as an unweighted sum of its Pauli words, so
QuantumOperator(["Z"], [0.5])returned the value for coefficient1.0. - PennyLane conditional gates never fired. The condition was read from
Instruction.condition, which Qiskit removed in 2.0, so conditioned gates were
silently applied unconditionally. - Classical conditions were dropped by gate lowering. A conditioned gate that
needed decomposing came out of the pass unconditional and ran on every shot. - Pauli propagation applied conditioned gates unconditionally rather than
reporting that the Heisenberg picture cannot express them. pauli_evolutionraisedTypeErroron a symbolic coefficient, because it
calledfloat()on an unbound parameter.- Sampling failed for any circuit with its own measurements. Counts were read
from a hard-codedmeasregister, which only exists for circuits the executor
measured itself. PauliPropagationOperatorreported on a placeholder representation. Its
terms live in a bit-packed store, butfingerprint(),ir,__len__and
is_hermitianwere inherited and read an all-identity placeholder: any two
operators of the same width had the same fingerprint,len(op)was always 1,
and a non-Hermitian operator reportedis_hermitian is True.- Converting a circuit could alias the original. The lowering pass returns
its input unchanged when nothing needs rewriting, so a converted circuit often
shared the instruction store it was converted from. statevector()now refuses circuits containing a measurement or reset. Qiskit
simulates a reset by drawing an outcome, so repeated calls returned different
vectors for the same circuit and the result cache froze whichever came first.
Migrating to the framework-independent layer
1. Install the Qiskit extra if you use the Qiskit backend
Qiskit is no longer a core dependency:
pip install "qc-executor[qiskit]" # statevector simulation
pip install "qc-executor[qiskit-full]" # adds Aer and the IBM RuntimeA core install now brings in only numpy and sympy. Pauli propagation is pure
Python and works with no extra at all.
2. Check your Pauli labels
This is the change most likely to alter your results silently. Qubit q is now
character q of the label on every backend:
QuantumOperator(["ZI"], [1.0]) # Z on qubit 0, I on qubit 1
QuantumOperator(["IZ"], [1.0]) # I on qubit 0, Z on qubit 1If you were using the Qiskit backend, labels that mixed different Paulis need
reversing; palindromic labels such as "ZZ" and "II" are unaffected. Code
written against PennyLane, Qulacs or Pauli propagation needs no change — those
three already read labels this way, and it was the Qiskit backend that was the
odd one out.
Raw SparsePauliOp objects passed straight to QiskitOperator keep Qiskit's own
convention; only QuantumOperator labels are affected.
3. Replace Qiskit parameter types
# Before
from qiskit.circuit import ParameterVector
x = ParameterVector("x", 2)
# After
from qc_executor import Parameters
x = Parameters("x", 2)Parameters elements are SymPy symbols, so build expressions with SymPy rather
than Qiskit:
import sympy as sp
circuit.rx(0, 2 * x[0]) # unchanged
circuit.ry(1, sp.sin(x[0])) # was x[0].sin()Executor keyword arguments are unchanged: executor.expectation_value(qc, obs, x=[0.1, 0.2]) still works, as does the indexed form
executor.expectation_value(qc, obs, **{"x[0]": 0.1, "x[1]": 0.2}).
Note that str(Parameters("x", 3)) now returns "x" rather than a rendering of
the whole vector.
4. Update measure() calls
measure() took no arguments and measured everything. It now takes explicit
qubits and classical bits:
circuit.measure_all() # closest to the old behaviour
circuit.measure(0, 0) # qubit 0 into classical bit 0
circuit.measure([0, 1]) # allocates classical bits automatically5. Convert with from_quantum_circuit, build with the constructor
Native constructors now take (num_qubits, num_clbits=0) like any circuit:
# Before
native = QulacsCircuit(generic_circuit)
# After -- converting
native = QulacsCircuit.from_quantum_circuit(generic_circuit)
# After -- building directly, which was not previously possible
native = QulacsCircuit(2)
native.h(0)The same applies to operators: QulacsOperator(generic_op) becomes
QulacsOperator.from_quantum_operator(generic_op), and
QulacsOperator(["ZI"], [1.0]) builds one directly.
6. assign_parameters returns a new circuit
# Before: bound in place, returned self
circuit.assign_parameters({x[0]: 0.5})
# After
bound = circuit.assign_parameters({x[0]: 0.5}) # circuit is unchanged7. compose no longer mutates
# Before: a was modified in place
a.compose(b)
# After
a = a.compose(b)8. Moved modules
These were internal, but they moved into the Qiskit plugin because only that
plugin speaks Qiskit:
| Before | After |
|---|---|
qc_executor.utils.qiskit_compat |
qc_executor.qiskit._compat |
qc_executor.utils.decompose_to_std |
qc_executor.qiskit._decompose |
qc_executor.utils.qiskit_hash_functions |
qc_executor.qiskit._hash |
qc_executor.utils.data_preprocessing.ensure_complex_coeffs |
qc_executor.qiskit._compat.ensure_complex_coeffs |
9. Escape hatches
The generic types can still produce Qiskit objects when the extra is installed:
circuit.qiskit_circuit # -> qiskit.QuantumCircuit
observable.qiskit_operator # -> qiskit.quantum_info.SparsePauliOpEvery executor method accepts a generic or a backend-native circuit and
operator, so an object you have already converted is passed straight through
rather than translated again.