Skip to content

Add generic analog circuit builder class #7491

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 14 commits into from
Jul 25, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions cirq-google/cirq_google/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@
PhysicalZTag as PhysicalZTag,
SYC as SYC,
SycamoreGate as SycamoreGate,
WaitGateWithUnit as WaitGateWithUnit,
WILLOW as WILLOW,
WillowGate as WillowGate,
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,7 @@
FrequencyMap as FrequencyMap,
AnalogTrajectory as AnalogTrajectory,
)

from cirq_google.experimental.analog_experiments.generic_analog_circuit import (
GenericAnalogCircuitBuilder as GenericAnalogCircuitBuilder,
)
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,13 @@ class FrequencyMap:
duration: duration of step
qubit_freqs: dict describing qubit frequencies at end of step (None if idle)
couplings: dict describing coupling rates at end of step
is_wait_step: a bool indicating only wait gate should be added.
"""

duration: su.ValueOrSymbol
qubit_freqs: dict[str, su.ValueOrSymbol | None]
couplings: dict[tuple[str, str], su.ValueOrSymbol]
is_wait_step: bool

def _is_parameterized_(self) -> bool:
return (
Expand Down Expand Up @@ -68,6 +70,7 @@ def _resolve_parameters_(
couplings={
k: su.direct_symbol_replacement(v, resolver_) for k, v in self.couplings.items()
},
is_wait_step=self.is_wait_step,
)


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

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

full_trajectory.append(FrequencyMap(dt, new_qubit_freq_dict, new_g_dict))
full_trajectory.append(FrequencyMap(dt, new_qubit_freq_dict, new_g_dict, is_wait_step))
return cls(full_trajectory=full_trajectory, qubits=qubits, pairs=pairs)

def get_full_trajectory_with_resolved_idles(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ def freq_map() -> atu.FrequencyMap:
10 * tu.ns,
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": sympy.Symbol("f_q0_2")},
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): sympy.Symbol("g_q0_1_q0_2")},
False,
)


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


Expand All @@ -52,36 +54,47 @@ def test_freq_map_resolve(freq_map: atu.FrequencyMap) -> None:
def sparse_trajectory() -> list[FreqMapType]:
traj1: FreqMapType = (20 * tu.ns, {"q0_1": 5 * tu.GHz}, {})
traj2: FreqMapType = (30 * tu.ns, {"q0_2": 8 * tu.GHz}, {})
traj3: FreqMapType = (
traj3: FreqMapType = (35 * tu.ns, {}, {})
traj4: FreqMapType = (
40 * tu.ns,
{"q0_0": 8 * tu.GHz, "q0_1": None, "q0_2": None},
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 8 * tu.MHz},
)
return [traj1, traj2, traj3]
return [traj1, traj2, traj3, traj4]


def test_full_traj(sparse_trajectory: list[FreqMapType]) -> None:
analog_traj = atu.AnalogTrajectory.from_sparse_trajectory(sparse_trajectory)
assert len(analog_traj.full_trajectory) == 4
assert len(analog_traj.full_trajectory) == 5
assert analog_traj.full_trajectory[0] == atu.FrequencyMap(
0 * tu.ns,
{"q0_0": None, "q0_1": None, "q0_2": None},
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
False,
)
assert analog_traj.full_trajectory[1] == atu.FrequencyMap(
20 * tu.ns,
{"q0_0": None, "q0_1": 5 * tu.GHz, "q0_2": None},
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
False,
)
assert analog_traj.full_trajectory[2] == atu.FrequencyMap(
30 * tu.ns,
{"q0_0": None, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
False,
)
assert analog_traj.full_trajectory[3] == atu.FrequencyMap(
35 * tu.ns,
{"q0_0": None, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
True,
)
assert analog_traj.full_trajectory[4] == atu.FrequencyMap(
40 * tu.ns,
{"q0_0": 8 * tu.GHz, "q0_1": None, "q0_2": None},
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 8 * tu.MHz},
False,
)


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

assert len(resolved_full_traj) == 4
assert len(resolved_full_traj) == 5
assert resolved_full_traj[0] == atu.FrequencyMap(
0 * tu.ns,
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 7 * tu.GHz},
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
False,
)
assert resolved_full_traj[1] == atu.FrequencyMap(
20 * tu.ns,
{"q0_0": 5 * tu.GHz, "q0_1": 5 * tu.GHz, "q0_2": 7 * tu.GHz},
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
False,
)
assert resolved_full_traj[2] == atu.FrequencyMap(
30 * tu.ns,
{"q0_0": 5 * tu.GHz, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
False,
)
assert resolved_full_traj[3] == atu.FrequencyMap(
35 * tu.ns,
{"q0_0": 5 * tu.GHz, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
True,
)
assert resolved_full_traj[4] == atu.FrequencyMap(
40 * tu.ns,
{"q0_0": 8 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 7 * tu.GHz},
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 8 * tu.MHz},
False,
)


Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
# Copyright 2025 The Cirq Developers
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import functools
import re

import cirq
from cirq_google.experimental.analog_experiments import analog_trajectory_util as atu
from cirq_google.ops import analog_detune_gates as adg, wait_gate as wg
from cirq_google.study import symbol_util as su


def _get_neighbor_freqs(
qubit_pair: tuple[str, str], qubit_freq_dict: dict[str, su.ValueOrSymbol | None]
) -> tuple[su.ValueOrSymbol | None, su.ValueOrSymbol | None]:
"""Get neighbor freqs from qubit_freq_dict given the pair."""
sorted_pair = sorted(qubit_pair, key=_to_grid_qubit)
return (qubit_freq_dict[sorted_pair[0]], qubit_freq_dict[sorted_pair[1]])


@functools.cache
def _to_grid_qubit(qubit_name: str) -> cirq.GridQubit:
match = re.compile(r"^q(\d+)_(\d+)$").match(qubit_name)
if match is None:
raise ValueError(f"Invalid qubit name format: '{qubit_name}'. Expected 'q<row>_<col>'.")
return cirq.GridQubit(int(match[1]), int(match[2]))


def _coupler_name_from_qubit_pair(qubit_pair: tuple[str, str]) -> str:
sorted_pair = sorted(qubit_pair, key=_to_grid_qubit)
return f"c_{sorted_pair[0]}_{sorted_pair[1]}"


def _get_neighbor_coupler_freqs(
qubit_name: str, coupler_g_dict: dict[tuple[str, str], su.ValueOrSymbol]
) -> dict[str, su.ValueOrSymbol]:
"""Get neighbor coupler coupling strength g given qubit name."""
return {
_coupler_name_from_qubit_pair(pair): g
for pair, g in coupler_g_dict.items()
if qubit_name in pair
}


class GenericAnalogCircuitBuilder:
"""Class for making arbitrary analog circuits. The circuit is defined by an
AnalogTrajectory object. The class constructs the circuit from AnalogDetune
pulses, which automatically calculate the necessary bias amps to both qubits
and couplers, using tu.Values from analog calibration whenever available.

Attributes:
trajectory: AnalogTrajectory object defining the circuit
g_ramp_shaping: coupling ramps are shaped according to ramp_shape_exp if True
qubits: list of qubits in the circuit
pairs: list of couplers in the circuit
ramp_shape_exp: exponent of g_ramp (g proportional to t^ramp_shape_exp)
interpolate_coupling_cal: interpolates between calibrated coupling tu.Values if True
linear_qubit_ramp: if True, the qubit ramp is linear. if false, a cosine shaped
ramp is used.
"""

def __init__(
self,
trajectory: atu.AnalogTrajectory,
g_ramp_shaping: bool = False,
ramp_shape_exp: int = 1,
interpolate_coupling_cal: bool = False,
linear_qubit_ramp: bool = True,
):
self.trajectory = trajectory
self.g_ramp_shaping = g_ramp_shaping
self.ramp_shape_exp = ramp_shape_exp
self.interpolate_coupling_cal = interpolate_coupling_cal
self.linear_qubit_ramp = linear_qubit_ramp

def make_circuit(self) -> cirq.Circuit:
"""Assemble moments described in trajectory."""
prev_freq_map = self.trajectory.full_trajectory[0]
moments = []
for freq_map in self.trajectory.full_trajectory[1:]:
if freq_map.is_wait_step:
targets = [_to_grid_qubit(q) for q in self.trajectory.qubits]
wait_gate = wg.WaitGateWithUnit(
freq_map.duration, qid_shape=cirq.qid_shape(targets)
)
moment = cirq.Moment(wait_gate.on(*targets))
else:
moment = self.make_one_moment(freq_map, prev_freq_map)
moments.append(moment)
prev_freq_map = freq_map

return cirq.Circuit.from_moments(*moments)

def make_one_moment(
self, freq_map: atu.FrequencyMap, prev_freq_map: atu.FrequencyMap
) -> cirq.Moment:
"""Make one moment of analog detune qubit and coupler gates given freqs."""
qubit_gates = []
for q, freq in freq_map.qubit_freqs.items():
qubit_gates.append(
adg.AnalogDetuneQubit(
length=freq_map.duration,
w=freq_map.duration,
target_freq=freq,
prev_freq=prev_freq_map.qubit_freqs.get(q),
neighbor_coupler_g_dict=_get_neighbor_coupler_freqs(q, freq_map.couplings),
prev_neighbor_coupler_g_dict=_get_neighbor_coupler_freqs(
q, prev_freq_map.couplings
),
linear_rise=self.linear_qubit_ramp,
).on(_to_grid_qubit(q))
)
coupler_gates = []
for p, g_max in freq_map.couplings.items():
# Currently skipping the step if these are the same.
# However, change in neighbor qubit freq could potentially change coupler amp
if g_max == prev_freq_map.couplings[p]:
continue

coupler_gates.append(
adg.AnalogDetuneCouplerOnly(
length=freq_map.duration,
w=freq_map.duration,
g_0=prev_freq_map.couplings[p],
g_max=g_max,
g_ramp_exponent=self.ramp_shape_exp,
neighbor_qubits_freq=_get_neighbor_freqs(p, freq_map.qubit_freqs),
prev_neighbor_qubits_freq=_get_neighbor_freqs(p, prev_freq_map.qubit_freqs),
interpolate_coupling_cal=self.interpolate_coupling_cal,
).on(*sorted([_to_grid_qubit(p[0]), _to_grid_qubit(p[1])]))
)

return cirq.Moment(qubit_gates + coupler_gates)
Loading