Qualtran
Qualtran copied to clipboard
Symbolic bitsizes for splits / joins / partitions?
Does it make sense for these to have symbolic register sizes?
The general philosophy is "as symbolic as possible". But: what would you expect the behavior to be here?
- decompose & call graph: doesn't matter because these are "atomic" / "leaf bloqs"
- simulation: an error
- drawing: this will cause an error message. probably a bad error message that we should improve. One side of it needs to have a symbolic number of soquets, which is impossible
- adding to a compositebloq: same as above
Things you can do:
- count qubits
- count t gates (zero)
I can't quite remember what I was thinking here, but I believe I had a case during block encoding where the register bitsize would potentially be symbolic but was needed for the input to the Partition bloq and then mypy was complaining, but I didn't think it necessarily made sense that Partition (or split / ..) expected a symbolic bitsize. In the end I rewrote the code so the issue didn't come up.
One benefit of symbolic split/join would be to remove some checks:
if is_symbolic(x):
raise DecomposeTypeError(...)
_ = bb.split(x)
in decompositions, which 1) makes the code cleaner and 2) makes it possible to derive the call graph automatically.
As an example, #1193 would not have been necessary if the split in build_composite_bloq could be instantiated with a symbolic parameter. The build_call_graph in that case just replicates the decomposition, skipping the split.
Going on a bit more of a limb, this would be a case where a QTuple data type would be useful. If we have a QAny(m) and a QAny(n), then a QTuple((QAny(m), QAny(n))) would have bitsize m + n completely agnostic of whether m and n are symbolic.
@charlesyuan314 in your example, what would the return value of bb.split(x) be? it's supposed to be an array. You can't have a (python, regular ol') array of symbolic length
In the second example, it would be bb.split(x) == np.array([y, z]) s.t. y.dtype == QAny(m) and z.dtype == QAny(n).
Perhaps this would coexist with the existing behavior of split which I would then rename bb.split_into_bits(x).
The first example would require some rewriting to follow the structure that the second example is aiming to exploit. So I guess it would not be simply removing the check and expecting the remaining code to work.
The spirit of the first example is that it should be possible in principle to automatically derive many more call graphs from decompositions if the decompositions can work even on registers of symbolic sizes.
The second scheme where we have a new tuple_split instead of the existing [bit]split is likely out of scope for the time being.
point taken that it would be nice to be able to have more "symbolic decompositions"; but splitting a register into bits means you're going to individually manipulate them in a way that requires knowing the actual quantity.
To support more symbolic decompositions we can have more types of bookkeeping bloqs like TakeN that always returns two soquets of size (k-n, n) and friends; but Split, and Join can't be used in a "symbolic decomposition"
Btw, Partition does work with symbolics:
import sympy
from attrs import frozen
from qualtran import Bloq, Signature
from qualtran.symbolics import SymbolicInt
from qualtran.bloqs.bookkeeping import Partition
from qualtran.bloqs.basic_gates import Identity
from qualtran.drawing import show_bloq
@frozen
class SymbolicSplit(Bloq):
n: SymbolicInt
m: SymbolicInt
@property
def signature(self):
return Signature.build(x=self.n + self.m)
@property
def partition(self):
regs = tuple(Signature.build(a=self.n, b=self.m))
return Partition(self.n + self.m, regs)
def build_composite_bloq(self, bb, x):
a, b = bb.add(self.partition, x=x)
a = bb.add(Identity(self.n), q=a)
b = bb.add(Identity(self.m), q=b)
x = bb.add(self.partition.adjoint(), a=a, b=b)
return {'x': x}
n, m = sympy.symbols("n m")
show_bloq(SymbolicSplit(n, m).decompose_bloq())
This works because sympy is able to compute equality. For instance, changing a=self.n + 1 would fail to decompose. It might fail in cases when two expressions are equal in principle, but sympy cannot simplify. But I'd say its safer to fail in those cases, rather than accept potentially mismatched symbolic sizes.
I think it makes sense to keep split/join concrete (no symbolic, n->1 or 1->n), and use Partition for more exotic use-cases.