Skip to content
Merged
Show file tree
Hide file tree
Changes from 7 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
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 @@ -19,6 +19,7 @@
import attrs
import matplotlib.pyplot as plt
import numpy as np
import sympy
import tunits as tu

import cirq
Expand All @@ -36,11 +37,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,8 +71,22 @@ 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,
)

def duration_nanos(self) -> float | su.TunitForDurationNanos:
# The following is the patching code for symbol/tunit can work
# with cirq.Duration object.
if isinstance(self.duration, tu.Value):
nanos = self.duration[tu.ns]
elif isinstance(self.duration, sympy.Symbol):
nanos = su.TunitForDurationNanos(self.duration)
else:
raise ValueError(
"The duration in the freq map must either be a tu.Value or a sympy.Symbol."
)
return nanos


class AnalogTrajectory:
"""Class for handling qubit frequency and coupling trajectories that
Expand Down Expand Up @@ -129,9 +146,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 +160,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 @@ -18,6 +18,7 @@

import cirq
from cirq_google.experimental.analog_experiments import analog_trajectory_util as atu
from cirq_google.study import symbol_util as su


@pytest.fixture
Expand All @@ -26,6 +27,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,46 +44,70 @@ 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,
)


def test_duration_nanos_in_freq_map() -> None:
fm = atu.FrequencyMap(10 * tu.ns, {}, {}, False)
assert fm.duration_nanos() == 10
fm = atu.FrequencyMap(sympy.Symbol("t"), {}, {}, False)
resolved_duration_nanos = cirq.resolve_parameters(fm.duration_nanos(), {"t": 10 * tu.ns})
assert isinstance(resolved_duration_nanos, su.TunitForDurationNanos)
assert resolved_duration_nanos.total_nanos() == 10

with pytest.raises(ValueError, match="either be a tu.Value or a sympy.Symbol"):
atu.FrequencyMap(10, {}, {}, False).duration_nanos()


FreqMapType = tuple[tu.Value, dict[str, tu.Value | None], dict[tuple[str, str], tu.Value]]


@pytest.fixture
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 +118,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,154 @@
# 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
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:
target = [_to_grid_qubit(q) for q in self.trajectory.qubits]
d = freq_map.duration_nanos()
if isinstance(d, float):
wait_gate = cirq.WaitGate(
cirq.Duration(nanos=d), qid_shape=cirq.qid_shape(target)
)
else:
# The following is patching solution for resolving the parameter
# can be tunits. It should only work for pyle internal translation.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems really hacky and I am worried that it might break assumptions elsewhere, such as in serialization.

Why can't we say something like:

elif isinstance(d, tu.Value):
    wait_gate = cirq.WaitGate(cirq.Duration(nanos=d[tu.ns], qid_shape=cirq.qid_shape(target))

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed offline, it is better to add a new gate that can accept the units

wait_gate = cirq.WaitGate(
cirq.Duration(nanos=1), qid_shape=cirq.qid_shape(target)
)
wait_gate._duration = d # type: ignore

moment = cirq.Moment(wait_gate.on(*target))
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