Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@
if TYPE_CHECKING:
from matplotlib.axes import Axes

from cirq_google.ops import coupler as cgc


@attrs.mutable
class FrequencyMap:
Expand All @@ -40,8 +42,8 @@ class FrequencyMap:
"""

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

def _is_parameterized_(self) -> bool:
Expand Down Expand Up @@ -86,8 +88,8 @@ def __init__(
self,
*,
full_trajectory: list[FrequencyMap],
qubits: list[str],
pairs: list[tuple[str, str]],
qubits: list[cirq.Qid],
pairs: list[cgc.Coupler],
Copy link
Collaborator

Choose a reason for hiding this comment

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

Maybe call it couplers instead of pairs?

Copy link
Collaborator Author

@BichengYing BichengYing Nov 8, 2025

Choose a reason for hiding this comment

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

Yeah, modified.

):
self.full_trajectory = full_trajectory
self.qubits = qubits
Expand All @@ -99,39 +101,39 @@ def from_sparse_trajectory(
sparse_trajectory: list[
tuple[
tu.Value,
dict[str, su.ValueOrSymbol | None],
dict[tuple[str, str], su.ValueOrSymbol],
dict[cirq.Qid, su.ValueOrSymbol | None],
dict[cgc.Coupler, su.ValueOrSymbol],
],
],
qubits: list[str] | None = None,
pairs: list[tuple[str, str]] | None = None,
qubits: list[cirq.Qid] | None = None,
pairs: list[cgc.Coupler] | None = None,
):
"""Construct AnalogTrajectory from sparse trajectory.

Args:
sparse_trajectory: A list of tuples, where each tuple defines a `FrequencyMap`
and contains three elements: (duration, qubit_freqs, coupling_strengths).
`duration` is a tunits value, `qubit_freqs` is a dictionary mapping qubit strings
Copy link
Collaborator

Choose a reason for hiding this comment

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

Please fix doc string ("qubit strings")

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

Copy link
Collaborator

Choose a reason for hiding this comment

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

Should we also accept cirq.Duration? If not, should we deprecate cirq.Duration?

https://quantumai.google/reference/python/cirq/Duration

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

We yse tu.Value here is requested by Trond because "duration" here is parameterized commonly. Resolving it into cirq.Duration is awkward but to t-units: it is simply cirq.Point("t_r", [1,2,3,4,5]*tu.ns)

to detuning frequencies, and `coupling_strengths` is a dictionary mapping qubit
pairs to their coupling strength. This format is considered "sparse" because each
to detuning frequencies, and `coupling_strengths` is a dictionary mapping
coupler to their coupling strength. This format is considered "sparse" because each
Copy link
Collaborator

Choose a reason for hiding this comment

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

Grammar: "coupler" --> "couplers"

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Done

tuple does not need to fully specify all qubits and coupling pairs; any missing
detuning frequency or coupling strength will be set to the same value as the
previous value in the list.
qubits: The qubits in interest. If not provided, automatically parsed from trajectory.
pairs: The pairs in interest. If not provided, automatically parsed from trajectory.
"""
if qubits is None or pairs is None:
qubits_in_traj: list[str] = []
pairs_in_traj: list[tuple[str, str]] = []
qubits_in_traj: list[cirq.Qid] = []
pairs_in_traj: list[cgc.Coupler] = []
for _, q, p in sparse_trajectory:
qubits_in_traj.extend(q.keys())
pairs_in_traj.extend(p.keys())
qubits = list(set(qubits_in_traj))
pairs = list(set(pairs_in_traj))

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}
init_qubit_freq_dict: dict[cirq.Qid, tu.Value | None] = {q: None for q in qubits}
init_g_dict: dict[cgc.Coupler, tu.Value] = {c: 0 * tu.MHz for c in pairs}
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:
Expand All @@ -142,15 +144,15 @@ def from_sparse_trajectory(
q: qubit_freq_dict.get(q, full_trajectory[-1].qubit_freqs.get(q)) for q in qubits
}
# If no g provided, set equal to previous
new_g_dict: dict[tuple[str, str], tu.Value] = {
p: g_dict.get(p, full_trajectory[-1].couplings.get(p)) for p in pairs # type: ignore[misc]
new_g_dict: dict[cgc.Coupler, tu.Value] = {
c: g_dict.get(c, full_trajectory[-1].couplings.get(c)) for c in pairs # type: ignore[misc]
}

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(
self, idle_freq_map: dict[str, tu.Value]
self, idle_freq_map: dict[cirq.Qid, tu.Value]
) -> list[FrequencyMap]:
"""Insert idle frequencies instead of None in trajectory."""

Expand All @@ -164,13 +166,19 @@ def get_full_trajectory_with_resolved_idles(

def plot(
self,
idle_freq_map: dict[str, tu.Value] | None = None,
default_idle_freq: tu.Value = 6.5 * tu.GHz,
idle_freq_map: dict[cirq.Qid, tu.Value] | None = None,
resolver: cirq.ParamResolverOrSimilarType | None = None,
axes: tuple[Axes, Axes] | None = None,
) -> tuple[Axes, Axes]:
if idle_freq_map is None:
idle_freq_map = {q: default_idle_freq for q in self.qubits}
# Because we use relative frequencies and we do not expose the idle frequencies,
# we randomly assign idle frequencies for plotting purposes only.
idle_freq_map = {q: np.random.randn() * 50 * tu.MHz for q in self.qubits}
else: # pragma: no cover
for q in self.qubits:
if q not in idle_freq_map: # Fill in missing idle freqs
idle_freq_map[q] = np.random.randn() * 50 * tu.MHz

full_trajectory_resolved = cirq.resolve_parameters(
self.get_full_trajectory_with_resolved_idles(idle_freq_map), resolver
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,19 @@
import tunits as tu

import cirq
import cirq_google as cg
from cirq_google.experimental.analog_experiments import analog_trajectory_util as atu


@pytest.fixture
def freq_map() -> atu.FrequencyMap:
return 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")},
{cirq.q(0, 0): 5 * tu.GHz, cirq.q(0, 1): 6 * tu.GHz, cirq.q(0, 2): sympy.Symbol("f_q0_2")},
{
cg.Coupler(cirq.q(0, 0), cirq.q(0, 1)): 5 * tu.MHz,
cg.Coupler(cirq.q(0, 1), cirq.q(0, 2)): sympy.Symbol("g_q0_1_q0_2"),
},
False,
)

Expand All @@ -41,59 +45,68 @@ def test_freq_map_resolve(freq_map: atu.FrequencyMap) -> None:
)
assert resolved_freq_map == atu.FrequencyMap(
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},
{cirq.q(0, 0): 5 * tu.GHz, cirq.q(0, 1): 6 * tu.GHz, cirq.q(0, 2): 6 * tu.GHz},
{
cg.Coupler(cirq.q(0, 0), cirq.q(0, 1)): 5 * tu.MHz,
cg.Coupler(cirq.q(0, 1), cirq.q(0, 2)): 7 * tu.MHz,
},
False,
)


FreqMapType = tuple[tu.Value, dict[str, tu.Value | None], dict[tuple[str, str], tu.Value]]
FreqMapType = tuple[tu.Value, dict[cirq.Qid, tu.Value | None], dict[cg.Coupler, 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}, {})
traj1: FreqMapType = (20 * tu.ns, {cirq.q(0, 1): 5 * tu.GHz}, {})
traj2: FreqMapType = (30 * tu.ns, {cirq.q(0, 2): 8 * tu.GHz}, {})
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},
{cirq.q(0, 0): 8 * tu.GHz, cirq.q(0, 1): None, cirq.q(0, 2): None},
{
cg.Coupler(cirq.q(0, 0), cirq.q(0, 1)): 5 * tu.MHz,
cg.Coupler(cirq.q(0, 1), cirq.q(0, 2)): 8 * tu.MHz,
},
)
return [traj1, traj2, traj3, traj4]


def test_full_traj(sparse_trajectory: list[FreqMapType]) -> None:
analog_traj = atu.AnalogTrajectory.from_sparse_trajectory(sparse_trajectory)
coupler1 = cg.Coupler(cirq.q(0, 0), cirq.q(0, 1))
coupler2 = cg.Coupler(cirq.q(0, 1), cirq.q(0, 2))

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},
{cirq.q(0, 0): None, cirq.q(0, 1): None, cirq.q(0, 2): None},
{coupler1: 0 * tu.MHz, coupler2: 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},
{cirq.q(0, 0): None, cirq.q(0, 1): 5 * tu.GHz, cirq.q(0, 2): None},
{coupler1: 0 * tu.MHz, coupler2: 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},
{cirq.q(0, 0): None, cirq.q(0, 1): 5 * tu.GHz, cirq.q(0, 2): 8 * tu.GHz},
{coupler1: 0 * tu.MHz, coupler2: 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},
{cirq.q(0, 0): None, cirq.q(0, 1): 5 * tu.GHz, cirq.q(0, 2): 8 * tu.GHz},
{coupler1: 0 * tu.MHz, coupler2: 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},
{cirq.q(0, 0): 8 * tu.GHz, cirq.q(0, 1): None, cirq.q(0, 2): None},
{coupler1: 5 * tu.MHz, coupler2: 8 * tu.MHz},
False,
)

Expand All @@ -102,53 +115,59 @@ def test_get_full_trajectory_with_resolved_idles(sparse_trajectory: list[FreqMap

analog_traj = atu.AnalogTrajectory.from_sparse_trajectory(sparse_trajectory)
resolved_full_traj = analog_traj.get_full_trajectory_with_resolved_idles(
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 7 * tu.GHz}
{cirq.q(0, 0): 5 * tu.GHz, cirq.q(0, 1): 6 * tu.GHz, cirq.q(0, 2): 7 * tu.GHz}
)
coupler1 = cg.Coupler(cirq.q(0, 0), cirq.q(0, 1))
coupler2 = cg.Coupler(cirq.q(0, 1), cirq.q(0, 2))

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},
{cirq.q(0, 0): 5 * tu.GHz, cirq.q(0, 1): 6 * tu.GHz, cirq.q(0, 2): 7 * tu.GHz},
{coupler1: 0 * tu.MHz, coupler2: 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},
{cirq.q(0, 0): 5 * tu.GHz, cirq.q(0, 1): 5 * tu.GHz, cirq.q(0, 2): 7 * tu.GHz},
{coupler1: 0 * tu.MHz, coupler2: 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},
{cirq.q(0, 0): 5 * tu.GHz, cirq.q(0, 1): 5 * tu.GHz, cirq.q(0, 2): 8 * tu.GHz},
{coupler1: 0 * tu.MHz, coupler2: 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},
{cirq.q(0, 0): 5 * tu.GHz, cirq.q(0, 1): 5 * tu.GHz, cirq.q(0, 2): 8 * tu.GHz},
{coupler1: 0 * tu.MHz, coupler2: 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},
{cirq.q(0, 0): 8 * tu.GHz, cirq.q(0, 1): 6 * tu.GHz, cirq.q(0, 2): 7 * tu.GHz},
{coupler1: 5 * tu.MHz, coupler2: 8 * tu.MHz},
False,
)


def test_plot_with_unresolved_parameters() -> None:
traj1: FreqMapType = (20 * tu.ns, {"q0_1": sympy.Symbol("qf")}, {})
traj2: FreqMapType = (sympy.Symbol("t"), {"q0_2": 8 * tu.GHz}, {})
traj1: FreqMapType = (20 * tu.ns, {cirq.q(0, 1): sympy.Symbol("qf")}, {})
traj2: FreqMapType = (sympy.Symbol("t"), {cirq.q(0, 2): 8 * tu.GHz}, {})
analog_traj = atu.AnalogTrajectory.from_sparse_trajectory([traj1, traj2])

with pytest.raises(ValueError):
analog_traj.plot()


def test_analog_traj_plot() -> None:
traj1: FreqMapType = (5 * tu.ns, {"q0_1": sympy.Symbol("qf")}, {("q0_0", "q0_1"): 2 * tu.MHz})
traj2: FreqMapType = (sympy.Symbol("t"), {"q0_2": 8 * tu.GHz}, {})
traj1: FreqMapType = (
5 * tu.ns,
{cirq.q(0, 1): sympy.Symbol("qf")},
{cg.Coupler(cirq.q(0, 0), cirq.q(0, 1)): 2 * tu.MHz},
)
traj2: FreqMapType = (sympy.Symbol("t"), {cirq.q(0, 2): 8 * tu.GHz}, {})
analog_traj = atu.AnalogTrajectory.from_sparse_trajectory([traj1, traj2])
analog_traj.plot(resolver={"t": 10 * tu.ns, "qf": 5 * tu.GHz})
Original file line number Diff line number Diff line change
Expand Up @@ -17,15 +17,15 @@

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.ops import analog_detune_gates as adg, coupler as cgc, 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]
coupler: cgc.Coupler, qubit_freq_dict: dict[cirq.Qid, 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)
sorted_pair = sorted(coupler.qubits)
return (qubit_freq_dict[sorted_pair[0]], qubit_freq_dict[sorted_pair[1]])


Expand All @@ -37,19 +37,19 @@ def _to_grid_qubit(qubit_name: str) -> cirq.GridQubit:
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 _coupler_name(coupler: cgc.Coupler) -> str:
q1, q2 = sorted(coupler.qubits)
return f"c_q{q1.row}_{q1.col}_q{q2.row}_{q2.col}" # type: ignore[attr-defined]
Comment on lines +40 to +42
Copy link
Collaborator

Choose a reason for hiding this comment

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

Do we need this function?

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Yeah, that is because analog code is still quite close to the internal code. The str coupler representation is still needed in the low-level gates implementation. (We don't wanna to introduce the circular import issue there)



def _get_neighbor_coupler_freqs(
qubit_name: str, coupler_g_dict: dict[tuple[str, str], su.ValueOrSymbol]
qubit: cirq.Qid, coupler_g_dict: dict[cgc.Coupler, 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
_coupler_name(coupler): g
for coupler, g in coupler_g_dict.items()
if qubit in coupler.qubits
}


Expand Down Expand Up @@ -90,7 +90,7 @@ def make_circuit(self) -> cirq.Circuit:
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]
targets = self.trajectory.qubits
wait_gate = wg.WaitGateWithUnit(
freq_map.duration, qid_shape=cirq.qid_shape(targets)
)
Expand Down Expand Up @@ -119,7 +119,7 @@ def make_one_moment(
q, prev_freq_map.couplings
),
linear_rise=self.linear_qubit_ramp,
).on(_to_grid_qubit(q))
).on(q)
)
coupler_gates = []
for p, g_max in freq_map.couplings.items():
Expand All @@ -138,7 +138,7 @@ def make_one_moment(
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])]))
).on(p)
)

return cirq.Moment(qubit_gates + coupler_gates)
Loading
Loading