Skip to content

Commit 45712a4

Browse files
authored
Add generic analog circuit builder class (#7491)
Re-write the generic analog circuit builder according to the analog trajectory object. Most are normal builder code, with one exception about the wait gate. 1. Add an indicator that one step (moment) is pure wait step based on the sparse trajectory input. <s>2. A hacky solution for the `cirq.WaitGate`. Because we want to use `tunit` for the duration, while `cirq.wait` only takes the duration object. I added a patch class that when it is resolving something like `54*tu.ns`, it will become 54. This can only works for `qcc.translate` because note the function there only needs two things: ```python duration = _resolve_parameters(gate.duration, resolver) duration_ns = duration.total_nanos() if duration.total_micros() > xxxxx: ... ``` </s> 2. In order for the WaitGate can work with tunits, added a new `WaitGateWithUnit` gate
1 parent 31df8ef commit 45712a4

File tree

13 files changed

+485
-6
lines changed

13 files changed

+485
-6
lines changed

cirq-google/cirq_google/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@
6464
PhysicalZTag as PhysicalZTag,
6565
SYC as SYC,
6666
SycamoreGate as SycamoreGate,
67+
WaitGateWithUnit as WaitGateWithUnit,
6768
WILLOW as WILLOW,
6869
WillowGate as WillowGate,
6970
)

cirq-google/cirq_google/experimental/analog_experiments/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@
1818
FrequencyMap as FrequencyMap,
1919
AnalogTrajectory as AnalogTrajectory,
2020
)
21+
22+
from cirq_google.experimental.analog_experiments.generic_analog_circuit import (
23+
GenericAnalogCircuitBuilder as GenericAnalogCircuitBuilder,
24+
)

cirq-google/cirq_google/experimental/analog_experiments/analog_trajectory_util.py

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@ class FrequencyMap:
3636
duration: duration of step
3737
qubit_freqs: dict describing qubit frequencies at end of step (None if idle)
3838
couplings: dict describing coupling rates at end of step
39+
is_wait_step: a bool indicating only wait gate should be added.
3940
"""
4041

4142
duration: su.ValueOrSymbol
4243
qubit_freqs: dict[str, su.ValueOrSymbol | None]
4344
couplings: dict[tuple[str, str], su.ValueOrSymbol]
45+
is_wait_step: bool
4446

4547
def _is_parameterized_(self) -> bool:
4648
return (
@@ -68,6 +70,7 @@ def _resolve_parameters_(
6870
couplings={
6971
k: su.direct_symbol_replacement(v, resolver_) for k, v in self.couplings.items()
7072
},
73+
is_wait_step=self.is_wait_step,
7174
)
7275

7376

@@ -129,9 +132,11 @@ def from_sparse_trajectory(
129132
full_trajectory: list[FrequencyMap] = []
130133
init_qubit_freq_dict: dict[str, tu.Value | None] = {q: None for q in qubits}
131134
init_g_dict: dict[tuple[str, str], tu.Value] = {p: 0 * tu.MHz for p in pairs}
132-
full_trajectory.append(FrequencyMap(0 * tu.ns, init_qubit_freq_dict, init_g_dict))
135+
full_trajectory.append(FrequencyMap(0 * tu.ns, init_qubit_freq_dict, init_g_dict, False))
133136

134137
for dt, qubit_freq_dict, g_dict in sparse_trajectory:
138+
# When both qubit_freq_dict and g_dict is empty, it is a wait step.
139+
is_wait_step = not (qubit_freq_dict or g_dict)
135140
# If no freq provided, set equal to previous
136141
new_qubit_freq_dict = {
137142
q: qubit_freq_dict.get(q, full_trajectory[-1].qubit_freqs.get(q)) for q in qubits
@@ -141,7 +146,7 @@ def from_sparse_trajectory(
141146
p: g_dict.get(p, full_trajectory[-1].couplings.get(p)) for p in pairs # type: ignore[misc]
142147
}
143148

144-
full_trajectory.append(FrequencyMap(dt, new_qubit_freq_dict, new_g_dict))
149+
full_trajectory.append(FrequencyMap(dt, new_qubit_freq_dict, new_g_dict, is_wait_step))
145150
return cls(full_trajectory=full_trajectory, qubits=qubits, pairs=pairs)
146151

147152
def get_full_trajectory_with_resolved_idles(

cirq-google/cirq_google/experimental/analog_experiments/analog_trajectory_util_test.py

Lines changed: 27 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ def freq_map() -> atu.FrequencyMap:
2626
10 * tu.ns,
2727
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": sympy.Symbol("f_q0_2")},
2828
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): sympy.Symbol("g_q0_1_q0_2")},
29+
False,
2930
)
3031

3132

@@ -42,6 +43,7 @@ def test_freq_map_resolve(freq_map: atu.FrequencyMap) -> None:
4243
10 * tu.ns,
4344
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 6 * tu.GHz},
4445
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 7 * tu.MHz},
46+
False,
4547
)
4648

4749

@@ -52,36 +54,47 @@ def test_freq_map_resolve(freq_map: atu.FrequencyMap) -> None:
5254
def sparse_trajectory() -> list[FreqMapType]:
5355
traj1: FreqMapType = (20 * tu.ns, {"q0_1": 5 * tu.GHz}, {})
5456
traj2: FreqMapType = (30 * tu.ns, {"q0_2": 8 * tu.GHz}, {})
55-
traj3: FreqMapType = (
57+
traj3: FreqMapType = (35 * tu.ns, {}, {})
58+
traj4: FreqMapType = (
5659
40 * tu.ns,
5760
{"q0_0": 8 * tu.GHz, "q0_1": None, "q0_2": None},
5861
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 8 * tu.MHz},
5962
)
60-
return [traj1, traj2, traj3]
63+
return [traj1, traj2, traj3, traj4]
6164

6265

6366
def test_full_traj(sparse_trajectory: list[FreqMapType]) -> None:
6467
analog_traj = atu.AnalogTrajectory.from_sparse_trajectory(sparse_trajectory)
65-
assert len(analog_traj.full_trajectory) == 4
68+
assert len(analog_traj.full_trajectory) == 5
6669
assert analog_traj.full_trajectory[0] == atu.FrequencyMap(
6770
0 * tu.ns,
6871
{"q0_0": None, "q0_1": None, "q0_2": None},
6972
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
73+
False,
7074
)
7175
assert analog_traj.full_trajectory[1] == atu.FrequencyMap(
7276
20 * tu.ns,
7377
{"q0_0": None, "q0_1": 5 * tu.GHz, "q0_2": None},
7478
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
79+
False,
7580
)
7681
assert analog_traj.full_trajectory[2] == atu.FrequencyMap(
7782
30 * tu.ns,
7883
{"q0_0": None, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
7984
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
85+
False,
8086
)
8187
assert analog_traj.full_trajectory[3] == atu.FrequencyMap(
88+
35 * tu.ns,
89+
{"q0_0": None, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
90+
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
91+
True,
92+
)
93+
assert analog_traj.full_trajectory[4] == atu.FrequencyMap(
8294
40 * tu.ns,
8395
{"q0_0": 8 * tu.GHz, "q0_1": None, "q0_2": None},
8496
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 8 * tu.MHz},
97+
False,
8598
)
8699

87100

@@ -92,26 +105,36 @@ def test_get_full_trajectory_with_resolved_idles(sparse_trajectory: list[FreqMap
92105
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 7 * tu.GHz}
93106
)
94107

95-
assert len(resolved_full_traj) == 4
108+
assert len(resolved_full_traj) == 5
96109
assert resolved_full_traj[0] == atu.FrequencyMap(
97110
0 * tu.ns,
98111
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 7 * tu.GHz},
99112
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
113+
False,
100114
)
101115
assert resolved_full_traj[1] == atu.FrequencyMap(
102116
20 * tu.ns,
103117
{"q0_0": 5 * tu.GHz, "q0_1": 5 * tu.GHz, "q0_2": 7 * tu.GHz},
104118
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
119+
False,
105120
)
106121
assert resolved_full_traj[2] == atu.FrequencyMap(
107122
30 * tu.ns,
108123
{"q0_0": 5 * tu.GHz, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
109124
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
125+
False,
110126
)
111127
assert resolved_full_traj[3] == atu.FrequencyMap(
128+
35 * tu.ns,
129+
{"q0_0": 5 * tu.GHz, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
130+
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
131+
True,
132+
)
133+
assert resolved_full_traj[4] == atu.FrequencyMap(
112134
40 * tu.ns,
113135
{"q0_0": 8 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 7 * tu.GHz},
114136
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 8 * tu.MHz},
137+
False,
115138
)
116139

117140

Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
# Copyright 2025 The Cirq Developers
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# https://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
import functools
16+
import re
17+
18+
import cirq
19+
from cirq_google.experimental.analog_experiments import analog_trajectory_util as atu
20+
from cirq_google.ops import analog_detune_gates as adg, wait_gate as wg
21+
from cirq_google.study import symbol_util as su
22+
23+
24+
def _get_neighbor_freqs(
25+
qubit_pair: tuple[str, str], qubit_freq_dict: dict[str, su.ValueOrSymbol | None]
26+
) -> tuple[su.ValueOrSymbol | None, su.ValueOrSymbol | None]:
27+
"""Get neighbor freqs from qubit_freq_dict given the pair."""
28+
sorted_pair = sorted(qubit_pair, key=_to_grid_qubit)
29+
return (qubit_freq_dict[sorted_pair[0]], qubit_freq_dict[sorted_pair[1]])
30+
31+
32+
@functools.cache
33+
def _to_grid_qubit(qubit_name: str) -> cirq.GridQubit:
34+
match = re.compile(r"^q(\d+)_(\d+)$").match(qubit_name)
35+
if match is None:
36+
raise ValueError(f"Invalid qubit name format: '{qubit_name}'. Expected 'q<row>_<col>'.")
37+
return cirq.GridQubit(int(match[1]), int(match[2]))
38+
39+
40+
def _coupler_name_from_qubit_pair(qubit_pair: tuple[str, str]) -> str:
41+
sorted_pair = sorted(qubit_pair, key=_to_grid_qubit)
42+
return f"c_{sorted_pair[0]}_{sorted_pair[1]}"
43+
44+
45+
def _get_neighbor_coupler_freqs(
46+
qubit_name: str, coupler_g_dict: dict[tuple[str, str], su.ValueOrSymbol]
47+
) -> dict[str, su.ValueOrSymbol]:
48+
"""Get neighbor coupler coupling strength g given qubit name."""
49+
return {
50+
_coupler_name_from_qubit_pair(pair): g
51+
for pair, g in coupler_g_dict.items()
52+
if qubit_name in pair
53+
}
54+
55+
56+
class GenericAnalogCircuitBuilder:
57+
"""Class for making arbitrary analog circuits. The circuit is defined by an
58+
AnalogTrajectory object. The class constructs the circuit from AnalogDetune
59+
pulses, which automatically calculate the necessary bias amps to both qubits
60+
and couplers, using tu.Values from analog calibration whenever available.
61+
62+
Attributes:
63+
trajectory: AnalogTrajectory object defining the circuit
64+
g_ramp_shaping: coupling ramps are shaped according to ramp_shape_exp if True
65+
qubits: list of qubits in the circuit
66+
pairs: list of couplers in the circuit
67+
ramp_shape_exp: exponent of g_ramp (g proportional to t^ramp_shape_exp)
68+
interpolate_coupling_cal: interpolates between calibrated coupling tu.Values if True
69+
linear_qubit_ramp: if True, the qubit ramp is linear. if false, a cosine shaped
70+
ramp is used.
71+
"""
72+
73+
def __init__(
74+
self,
75+
trajectory: atu.AnalogTrajectory,
76+
g_ramp_shaping: bool = False,
77+
ramp_shape_exp: int = 1,
78+
interpolate_coupling_cal: bool = False,
79+
linear_qubit_ramp: bool = True,
80+
):
81+
self.trajectory = trajectory
82+
self.g_ramp_shaping = g_ramp_shaping
83+
self.ramp_shape_exp = ramp_shape_exp
84+
self.interpolate_coupling_cal = interpolate_coupling_cal
85+
self.linear_qubit_ramp = linear_qubit_ramp
86+
87+
def make_circuit(self) -> cirq.Circuit:
88+
"""Assemble moments described in trajectory."""
89+
prev_freq_map = self.trajectory.full_trajectory[0]
90+
moments = []
91+
for freq_map in self.trajectory.full_trajectory[1:]:
92+
if freq_map.is_wait_step:
93+
targets = [_to_grid_qubit(q) for q in self.trajectory.qubits]
94+
wait_gate = wg.WaitGateWithUnit(
95+
freq_map.duration, qid_shape=cirq.qid_shape(targets)
96+
)
97+
moment = cirq.Moment(wait_gate.on(*targets))
98+
else:
99+
moment = self.make_one_moment(freq_map, prev_freq_map)
100+
moments.append(moment)
101+
prev_freq_map = freq_map
102+
103+
return cirq.Circuit.from_moments(*moments)
104+
105+
def make_one_moment(
106+
self, freq_map: atu.FrequencyMap, prev_freq_map: atu.FrequencyMap
107+
) -> cirq.Moment:
108+
"""Make one moment of analog detune qubit and coupler gates given freqs."""
109+
qubit_gates = []
110+
for q, freq in freq_map.qubit_freqs.items():
111+
qubit_gates.append(
112+
adg.AnalogDetuneQubit(
113+
length=freq_map.duration,
114+
w=freq_map.duration,
115+
target_freq=freq,
116+
prev_freq=prev_freq_map.qubit_freqs.get(q),
117+
neighbor_coupler_g_dict=_get_neighbor_coupler_freqs(q, freq_map.couplings),
118+
prev_neighbor_coupler_g_dict=_get_neighbor_coupler_freqs(
119+
q, prev_freq_map.couplings
120+
),
121+
linear_rise=self.linear_qubit_ramp,
122+
).on(_to_grid_qubit(q))
123+
)
124+
coupler_gates = []
125+
for p, g_max in freq_map.couplings.items():
126+
# Currently skipping the step if these are the same.
127+
# However, change in neighbor qubit freq could potentially change coupler amp
128+
if g_max == prev_freq_map.couplings[p]:
129+
continue
130+
131+
coupler_gates.append(
132+
adg.AnalogDetuneCouplerOnly(
133+
length=freq_map.duration,
134+
w=freq_map.duration,
135+
g_0=prev_freq_map.couplings[p],
136+
g_max=g_max,
137+
g_ramp_exponent=self.ramp_shape_exp,
138+
neighbor_qubits_freq=_get_neighbor_freqs(p, freq_map.qubit_freqs),
139+
prev_neighbor_qubits_freq=_get_neighbor_freqs(p, prev_freq_map.qubit_freqs),
140+
interpolate_coupling_cal=self.interpolate_coupling_cal,
141+
).on(*sorted([_to_grid_qubit(p[0]), _to_grid_qubit(p[1])]))
142+
)
143+
144+
return cirq.Moment(qubit_gates + coupler_gates)

0 commit comments

Comments
 (0)