2121from typing import cast , Dict , Iterable , Iterator , List , overload , Tuple , Union
2222
2323import attrs
24- import sympy
2524from attrs import field , frozen
2625
2726from qualtran .symbolics import is_symbolic , prod , smax , ssum , SymbolicInt
2827
29- from .data_types import QAny , QBit , QCDType
28+ from .data_types import QAny , QBit , QCDType , ShapedQCDType
3029
3130
3231class Side (enum .Flag ):
@@ -53,6 +52,12 @@ def __repr__(self):
5352 return f'{ self .__class__ .__name__ } .{ self ._name_ } '
5453
5554
55+ def _consume_register_dtype (dtype : Union [QCDType , ShapedQCDType ]) -> QCDType :
56+ # In __attrs_post_init__, we actually handle the ShapedQCDType case, which isn't accounted
57+ # for in attrs type checking.
58+ return cast (QCDType , dtype )
59+
60+
5661@frozen
5762class Register :
5863 """A register serves as the input/output quantum data specifications in a bloq's `Signature`.
@@ -72,7 +77,7 @@ class Register:
7277 """
7378
7479 name : str
75- dtype : QCDType
80+ dtype : QCDType = field ( converter = _consume_register_dtype )
7681 _shape : Tuple [SymbolicInt , ...] = field (
7782 default = tuple (), converter = lambda v : (v ,) if isinstance (v , int ) else tuple (v )
7883 )
@@ -86,6 +91,15 @@ def __repr__(self):
8691 return f'Register({ self .name !r} , dtype={ self .dtype !r} , shape={ self ._shape !r} , side={ self .side !r} )'
8792
8893 def __attrs_post_init__ (self ):
94+ if isinstance (self .dtype , ShapedQCDType ):
95+ if self ._shape != ():
96+ raise ValueError (
97+ f"for Register { self .name } , use either a shaped dtype { self .dtype } "
98+ f"or an explicit shape argument { self ._shape } , not both."
99+ )
100+ object .__setattr__ (self , '_shape' , self .dtype .shape )
101+ object .__setattr__ (self , 'dtype' , self .dtype .qcdtype )
102+
89103 if not isinstance (self .dtype , QCDType ):
90104 raise ValueError (f'dtype must be a QCDType: found { type (self .dtype )} ' )
91105
@@ -193,13 +207,15 @@ def __init__(self, registers: Iterable[Register]):
193207 self ._rights = _dedupe ((reg .name , reg ) for reg in self ._registers if reg .side & Side .RIGHT )
194208
195209 @classmethod
196- def build (cls , ** registers : Union [ int , sympy . Expr ] ) -> 'Signature' :
197- """Construct a Signature comprised of untyped thru registers of the given bitsizes .
210+ def build (cls , * args , ** kwargs ) -> 'Signature' :
211+ """Construct a Signature using a more natural syntax .
198212
199- For rapid prorotyping or simple gates, this syntactic sugar can be used.
213+ This builder constructs a `Signature` flexibly from a mix of types, positional elements,
214+ and named keyword arguments. For rapid prototyping or simple gates, you can quickly define
215+ registers without manually instantiating `Register` objects.
200216
201217 Examples:
202- The following constructors are equivalent
218+ The following constructors are equivalent:
203219
204220 >>> sig1 = Signature.build(a=32, b=1)
205221 >>> sig2 = Signature([
@@ -209,13 +225,104 @@ def build(cls, **registers: Union[int, sympy.Expr]) -> 'Signature':
209225 >>> sig1 == sig2
210226 True
211227
228+ We can also build signatures with fully instantiated `QCDType` arguments, including
229+ shaped multidimensional registers:
230+
231+ >>> from qualtran import QBit, QUInt
232+ >>> sig = Signature.build(ctrl=QBit()[5, 5], system=QUInt(32))
233+ >>> sig == Signature([
234+ ... Register('ctrl', QBit(), shape=(5, 5)),
235+ ... Register('system', QUInt(32))
236+ ... ])
237+ True
238+
239+ Left and Right registers can be specified with a 2-tuple `(LEFT, RIGHT)`.
240+ Here, we allocate `b` as a right register.
241+
242+ >>> sig = Signature.build(a=(QBit(), QBit()), b=(None, QBit()))
243+ >>> sig == Signature([
244+ ... Register('a', QBit(), side=Side.THRU),
245+ ... Register('b', QBit(), side=Side.RIGHT)
246+ ... ])
247+ True
248+
249+ Positional arguments can be used to join previously defined components:
250+
251+ >>> sig1 = Signature.build(a=1)
252+ >>> extra = [Register('c', QAny(5))]
253+ >>> sig2 = Signature.build(sig1, extra)
254+
212255 Args:
213- **registers: Keyword arguments mapping register names to bitsizes. All registers
214- will be 0-dimensional, THRU, and of type QAny/QBit.
256+ *args: Positional arguments must be instances of `Register`, `Signature`, or iterables
257+ thereof, which will be concatenated in order of layout.
258+ **kwargs: Keyword arguments mapping register names to data types or sizes.
259+ Values can be integer bitsizes (where 1 maps to `QBit` and n to `QAny(n)`),
260+ `QCDType` instances, `ShapedQCDType` instances, or 2-tuples of
261+ `(left_dtype, right_dtype)` to explicitly specify sides.
215262 """
216- return cls (
217- Register (name = k , dtype = QBit () if v == 1 else QAny (v )) for k , v in registers .items () if v
218- )
263+ if args and kwargs :
264+ raise ValueError (
265+ f"When using `Signature.build`, you must either specify a mapping "
266+ f"from register names to data types or positional Signature and "
267+ f"Register arguments, not both. Found positional { args } and keyword { kwargs } "
268+ )
269+
270+ registers = []
271+
272+ def _flat_add (arg ):
273+ # add positional Signature, Register, or iterables thereof.
274+ nonlocal registers
275+ if isinstance (arg , Register ):
276+ registers .append (arg )
277+ elif isinstance (arg , Signature ):
278+ registers .extend (arg )
279+ elif isinstance (arg , Iterable ) and not isinstance (arg , str ):
280+ for a2 in arg :
281+ _flat_add (a2 )
282+ else :
283+ raise ValueError (
284+ f"Unknown type for positional argument to Signature.build: { arg !r} "
285+ )
286+
287+ if args :
288+ for arg in args :
289+ _flat_add (arg )
290+ return cls (registers )
291+
292+ for k , v in kwargs .items ():
293+ if not v :
294+ continue
295+
296+ if isinstance (v , (QCDType , ShapedQCDType )):
297+ registers .append (Register (name = k , dtype = v ))
298+ elif isinstance (v , tuple ):
299+ if len (v ) != 2 :
300+ raise ValueError (
301+ f"When using Signature.build with a tuple of data types, "
302+ f"you must specify a tuple of length 2. For LEFT registers, "
303+ f"the tuple is (dtype, None). For RIGHT registers, "
304+ f"the tuple is (None, dtype). You provided { v } "
305+ )
306+ ldt , rdt = v
307+ if ldt is not None :
308+ registers .append (Register (name = k , dtype = ldt , side = Side .LEFT ))
309+ if rdt is not None :
310+ registers .append (Register (name = k , dtype = rdt , side = Side .RIGHT ))
311+
312+ elif isinstance (v , (Register , Signature )):
313+ # mild defensiveness against common errors, but duck typing in the `else` clause.
314+ raise ValueError (
315+ f"Invalid data type for Signature.build keyword argument '{ k } ': { v } "
316+ )
317+ else :
318+ dt : QCDType
319+ if v == 1 :
320+ dt = QBit ()
321+ else :
322+ dt = QAny (v )
323+ registers .append (Register (name = k , dtype = dt ))
324+
325+ return cls (registers )
219326
220327 @classmethod
221328 def build_from_dtypes (cls , ** registers : QCDType ) -> 'Signature' :
@@ -323,12 +430,10 @@ def __repr__(self):
323430 return f'Signature({ repr (self ._registers )} )'
324431
325432 @overload
326- def __getitem__ (self , key : int ) -> Register :
327- pass
433+ def __getitem__ (self , key : int ) -> Register : ...
328434
329435 @overload
330- def __getitem__ (self , key : slice ) -> Tuple [Register , ...]:
331- pass
436+ def __getitem__ (self , key : slice ) -> Tuple [Register , ...]: ...
332437
333438 def __getitem__ (self , key ):
334439 return self ._registers [key ]
0 commit comments