Skip to content

Commit 378291b

Browse files
Copilotfgfuchs
andcommitted
Fix setNumQubits requirement, add annotation support, update README with lego example
Co-authored-by: fgfuchs <2428162+fgfuchs@users.noreply.github.com>
1 parent 2d3885d commit 378291b

File tree

8 files changed

+368
-24
lines changed

8 files changed

+368
-24
lines changed

README.md

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -166,15 +166,58 @@ Assuming all-to-all connectivity of qubits, one can minimize the depth of the ci
166166
![this graph](images/minimal_depth.png "Edge Coloring")
167167

168168

169+
***
170+
### Building circuits like Lego
171+
172+
Components can be freely composed ("lego style") to build more complex circuits without needing to configure qubit counts manually.
173+
174+
For example, a Grover mixer over 3 independent Dicke-2 sub-registers can be assembled like this:
175+
176+
from qaoa import initialstates, mixers
177+
178+
dicke = initialstates.Dicke(2) # Dicke state with k=2 excitations
179+
grover = mixers.Grover(dicke) # Grover mixer over the Dicke feasible space
180+
tensor = initialstates.Tensor(grover, 3) # 3 independent copies (6 qubits total)
181+
182+
tensor.create_circuit()
183+
tensor.circuit.draw('mpl')
184+
185+
`Dicke(k)` automatically sets `N_qubits = k` (the minimum needed register), and
186+
`Grover` inherits that qubit count from its sub-circuit, so no explicit
187+
`setNumQubits` call is required.
188+
189+
![Lego circuit](images/lego_circuit.png "Lego circuit: three Grover blocks on 6 qubits")
190+
191+
Each sub-circuit is shown as a labelled block in the drawing so the "lego" structure
192+
is immediately visible.
193+
194+
### Annotating circuits
195+
196+
Every component (initial state or mixer) carries a `label` attribute that is used as
197+
the circuit name when `create_circuit()` is called. The label defaults to the class
198+
name but can be customised at construction time (for `Dicke`, `Grover`, and `Tensor`)
199+
or by setting the attribute before calling `create_circuit()`:
200+
201+
dicke = initialstates.Dicke(2, label="Dicke-2")
202+
dicke.create_circuit()
203+
print(dicke.circuit.name) # → "Dicke-2"
204+
205+
xy = mixers.XY()
206+
xy.label = "XY-ring"
207+
xy.setNumQubits(4)
208+
xy.create_circuit()
209+
print(xy.circuit.name) # → "XY-ring"
210+
169211
***
170212
### Tensorize mixers
171213
To tensorize a mixer, i.e. decomposing the mixer into a tensor product of unitaries that is
172-
performed on each qubit, one can call the tensor class with the arguments of mixer and number of qubits in subpart.
214+
performed on each qubit, one can call the `Tensor` class with the sub-circuit and the number of copies.
173215

174216
For example, for the standard MaxCut problem above where the X mixer was used, one could find the tensor by writing:
175217

176-
tensorized_mixer = Tensor(mixer.X(), number_of_qubits_of_subpart)
177-
<!--find out the number of qubits we want here -->
218+
x_mixer = mixers.X()
219+
x_mixer.setNumQubits(number_of_qubits_of_subpart)
220+
tensorized_mixer = initialstates.Tensor(x_mixer, number_of_copies)
178221

179222
***
180223
### Talk to an agent

images/lego_circuit.png

26.6 KB
Loading

qaoa/initialstates/base_initialstate.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,24 @@ class BaseInitialState(ABC):
1212
Attributes:
1313
circuit (QuantumCircuit): The quantum circuit representing the initial state.
1414
N_qubits (int): The number of qubits in the quantum circuit.
15+
label (str): A human-readable annotation for this component, used as the
16+
circuit name when ``create_circuit`` is called. Defaults to the
17+
class name.
1518
"""
1619

17-
def __init__(self) -> None:
20+
def __init__(self, label: str | None = None) -> None:
1821
"""
1922
Initializes a BaseInitialState object.
2023
21-
The `circuit` attribute is set to None initially, and `N_qubits`
22-
is not defined until `setNumQubits` is called.
24+
The ``circuit`` attribute is set to None initially, and ``N_qubits``
25+
is not defined until ``setNumQubits`` is called.
26+
27+
Args:
28+
label (str | None): Optional annotation label for this component.
29+
Defaults to the class name when *None*.
2330
"""
2431
self.circuit = None
32+
self.label = label if label is not None else self.__class__.__name__
2533

2634
def setNumQubits(self, n):
2735
"""
@@ -55,6 +63,55 @@ def create_circuit(self):
5563
```
5664
"""
5765

66+
def __init_subclass__(cls, **kwargs):
67+
"""
68+
Automatically equips every concrete subclass with two behaviours:
69+
70+
1. **Default label** – if the subclass ``__init__`` does *not* call
71+
``super().__init__()``, the instance will still get a ``label``
72+
attribute (set to the class name) after ``__init__`` returns.
73+
2. **Circuit annotation** – after ``create_circuit`` returns the
74+
``circuit.name`` attribute is set to ``self.label`` so that
75+
circuit drawings show the component name.
76+
"""
77+
super().__init_subclass__(**kwargs)
78+
79+
# Wrap __init__ to guarantee self.label exists even when super() is
80+
# not called by the subclass.
81+
if "__init__" in cls.__dict__:
82+
_orig_init = cls.__dict__["__init__"]
83+
84+
def _make_init_wrapper(f):
85+
def _wrapped_init(self, *args, **kwargs):
86+
f(self, *args, **kwargs)
87+
if not hasattr(self, "label"):
88+
self.label = self.__class__.__name__
89+
90+
_wrapped_init.__doc__ = f.__doc__
91+
_wrapped_init.__name__ = f.__name__
92+
return _wrapped_init
93+
94+
setattr(cls, "__init__", _make_init_wrapper(_orig_init))
95+
96+
# Wrap create_circuit to set circuit.name from self.label.
97+
if "create_circuit" in cls.__dict__:
98+
_orig_cc = cls.__dict__["create_circuit"]
99+
if not getattr(_orig_cc, "__isabstractmethod__", False):
100+
101+
def _make_cc_wrapper(f):
102+
def _wrapped_cc(self, *args, **kwargs):
103+
f(self, *args, **kwargs)
104+
if self.circuit is not None:
105+
self.circuit.name = getattr(
106+
self, "label", self.__class__.__name__
107+
)
108+
109+
_wrapped_cc.__doc__ = f.__doc__
110+
_wrapped_cc.__name__ = f.__name__
111+
return _wrapped_cc
112+
113+
setattr(cls, "create_circuit", _make_cc_wrapper(_orig_cc))
114+
58115
@abstractmethod
59116
def create_circuit(self):
60117
"""

qaoa/initialstates/dicke_initialstate.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,18 @@ class Dicke(InitialState):
1919
Methods:
2020
create_circuit(): Creates the circuit to prepare the Dicke states.
2121
"""
22-
def __init__(self, k) -> None:
22+
def __init__(self, k, label: str | None = None) -> None:
2323
"""
2424
Args:
2525
k (int): The Hamming weight of the Dicke states.
26+
label (str | None): Optional annotation label. Defaults to
27+
``"Dicke"``.
2628
"""
27-
super().__init__()
29+
super().__init__(label=label)
2830
self.k = k
31+
# Default N_qubits to k (minimum register needed for k excitations).
32+
# Can be overridden via setNumQubits().
33+
self.N_qubits = k
2934

3035
def create_circuit(self):
3136
"""

qaoa/initialstates/tensor_initialstate.py

Lines changed: 27 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -10,31 +10,48 @@ class Tensor(InitialState):
1010
"""
1111
Tensor initial state.
1212
13-
Subclass of the `IntialState` class that creates a tensor out of a circuit.
13+
Subclass of the `InitialState` class that creates a tensor product of
14+
*num* copies of a sub-circuit (initial state **or** mixer) placed
15+
side-by-side on disjoint qubit registers.
16+
17+
When drawn, each copy is shown as a labelled box using the sub-circuit's
18+
own ``label`` attribute, making the "lego" structure immediately visible.
1419
1520
Attributions:
1621
subcircuit (InitialState): The circuit that is to be tensorised.
17-
num (int): Number of qubits of the subpart .
22+
num (int): Number of copies of the sub-circuit.
1823
1924
Methods:
20-
create_circuit():
25+
create_circuit(): Creates the tensorised circuit.
2126
"""
22-
def __init__(self, subcircuit: InitialState, num: int) -> None:
27+
28+
def __init__(self, subcircuit: InitialState, num: int, label: str | None = None) -> None:
2329
"""
2430
Args:
2531
subcircuit (InitialState): The circuit that is to be tensorised.
26-
num (int): Number of qubits of the subpart #subN_qubits.
32+
num (int): Number of copies of the sub-circuit.
33+
label (str | None): Optional annotation label. Defaults to
34+
``"Tensor"``.
2735
"""
36+
super().__init__(label=label)
2837
self.num = num
2938
self.subcircuit = subcircuit
3039
self.N_qubits = self.num * self.subcircuit.N_qubits
3140

3241
def create_circuit(self) -> None:
3342
"""
34-
Creates a circuit that tensorises a given subcircuit.
43+
Creates a circuit that places *num* copies of the sub-circuit on
44+
disjoint qubit registers.
45+
46+
Each copy is wrapped as a labelled instruction so that circuit
47+
drawings show named "lego" blocks instead of raw gates.
3548
"""
3649
self.subcircuit.create_circuit()
37-
self.circuit = self.subcircuit.circuit
38-
for v in range(self.num - 1):
39-
self.subcircuit.create_circuit() # self.subcircuit.circuit.qregs)
40-
self.circuit.tensor(self.subcircuit.circuit, inplace=True)
50+
sub_label = getattr(self.subcircuit, "label", self.subcircuit.__class__.__name__)
51+
sub_instr = self.subcircuit.circuit.to_instruction(label=sub_label)
52+
53+
qr = QuantumRegister(self.N_qubits)
54+
self.circuit = QuantumCircuit(qr)
55+
n = self.subcircuit.N_qubits
56+
for i in range(self.num):
57+
self.circuit.append(sub_instr, list(range(i * n, (i + 1) * n)))

qaoa/mixers/base_mixer.py

Lines changed: 60 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,16 +12,24 @@ class MixerBase(ABC):
1212
Attributes:
1313
circuit (QuantumCircuit): The quantum circuit associated with the mixer.
1414
N_qubits (int): The number of qubits in the mixer circuit.
15+
label (str): A human-readable annotation for this component, used as the
16+
circuit name when ``create_circuit`` is called. Defaults to the
17+
class name.
1518
"""
1619

17-
def __init__(self) -> None:
20+
def __init__(self, label: str | None = None) -> None:
1821
"""
1922
Initializes a MixerBase object.
2023
21-
The `circuit` attribute is set to None initially, and `N_qubits`
22-
is not defined until `setNumQubits` is called.
24+
The ``circuit`` attribute is set to None initially, and ``N_qubits``
25+
is not defined until ``setNumQubits`` is called.
26+
27+
Args:
28+
label (str | None): Optional annotation label for this component.
29+
Defaults to the class name when *None*.
2330
"""
2431
self.circuit = None
32+
self.label = label if label is not None else self.__class__.__name__
2533

2634
def setNumQubits(self, n):
2735
"""
@@ -61,6 +69,55 @@ def create_circuit(self):
6169
```
6270
"""
6371

72+
def __init_subclass__(cls, **kwargs):
73+
"""
74+
Automatically equips every concrete subclass with two behaviours:
75+
76+
1. **Default label** – if the subclass ``__init__`` does *not* call
77+
``super().__init__()``, the instance will still get a ``label``
78+
attribute (set to the class name) after ``__init__`` returns.
79+
2. **Circuit annotation** – after ``create_circuit`` returns the
80+
``circuit.name`` attribute is set to ``self.label`` so that
81+
circuit drawings show the component name.
82+
"""
83+
super().__init_subclass__(**kwargs)
84+
85+
# Wrap __init__ to guarantee self.label exists even when super() is
86+
# not called by the subclass.
87+
if "__init__" in cls.__dict__:
88+
_orig_init = cls.__dict__["__init__"]
89+
90+
def _make_init_wrapper(f):
91+
def _wrapped_init(self, *args, **kwargs):
92+
f(self, *args, **kwargs)
93+
if not hasattr(self, "label"):
94+
self.label = self.__class__.__name__
95+
96+
_wrapped_init.__doc__ = f.__doc__
97+
_wrapped_init.__name__ = f.__name__
98+
return _wrapped_init
99+
100+
setattr(cls, "__init__", _make_init_wrapper(_orig_init))
101+
102+
# Wrap create_circuit to set circuit.name from self.label.
103+
if "create_circuit" in cls.__dict__:
104+
_orig_cc = cls.__dict__["create_circuit"]
105+
if not getattr(_orig_cc, "__isabstractmethod__", False):
106+
107+
def _make_cc_wrapper(f):
108+
def _wrapped_cc(self, *args, **kwargs):
109+
f(self, *args, **kwargs)
110+
if self.circuit is not None:
111+
self.circuit.name = getattr(
112+
self, "label", self.__class__.__name__
113+
)
114+
115+
_wrapped_cc.__doc__ = f.__doc__
116+
_wrapped_cc.__name__ = f.__name__
117+
return _wrapped_cc
118+
119+
setattr(cls, "create_circuit", _make_cc_wrapper(_orig_cc))
120+
64121
@abstractmethod
65122
def create_circuit(self):
66123
"""

qaoa/mixers/grover_mixer.py

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,24 @@ class Grover(Mixer):
2121
create_circuit(): Constructs the Grover mixer circuit using the subcircuit.
2222
"""
2323

24-
def __init__(self, subcircuit: InitialState) -> None:
24+
def __init__(self, subcircuit: InitialState, label: str | None = None) -> None:
2525
"""
2626
Initializes the Grover mixer.
2727
2828
Args:
29-
subcircuit (InitialState): The initial state circuit.
29+
subcircuit (InitialState): The initial state circuit. If the
30+
subcircuit already has ``N_qubits`` set the Grover mixer
31+
inherits it automatically so that ``setNumQubits`` does not
32+
need to be called separately.
33+
label (str | None): Optional annotation label. Defaults to
34+
``"Grover"``.
3035
"""
36+
super().__init__(label=label)
3137
self.subcircuit = subcircuit
3238
self.mixer_param = Parameter("x_beta")
39+
# Inherit N_qubits from the subcircuit when it is already known.
40+
if hasattr(subcircuit, "N_qubits"):
41+
self.N_qubits = subcircuit.N_qubits
3342

3443
def create_circuit(self):
3544
r"""
@@ -40,7 +49,10 @@ def create_circuit(self):
4049
The Grover mixer has the form US^\dagger X^n C^{n-1}Phase X^n US.
4150
"""
4251

43-
self.subcircuit.setNumQubits(self.N_qubits)
52+
# Only update the subcircuit's qubit count when it differs from our own,
53+
# preserving any N_qubits that was already set on the subcircuit.
54+
if not hasattr(self.subcircuit, "N_qubits") or self.subcircuit.N_qubits != self.N_qubits:
55+
self.subcircuit.setNumQubits(self.N_qubits)
4456
self.subcircuit.create_circuit()
4557
US = self.subcircuit.circuit
4658

0 commit comments

Comments
 (0)