Skip to content

Commit 5c96d02

Browse files
authored
Add AnalogTrajectory and FreqMap (#7437)
Add FreqMap and AnalogTrajectory into cirq_google.experiments. In this PR, two major differences compared with Trond did before. 1. `AnalogTrajectory` initialized with classmethod so that the class member is always full_trajectory 2. Add the resolving ability in the `FreqMap` so that we can provide the symbol into it and plot it with resolver like `pulse_plot` New usage example ```py traj1 = (20 * tu.ns, {"q0_1": 5 * tu.GHz}, {}) traj2 = (sympy.Symbol("t1"), {"q0_2": 8 * tu.GHz}, {}) traj3 = ( 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}, ) trajs = [traj1, traj2, traj3] analog_traj = AnalogTrajectory.from_sparse_trajecotry(trajs) analog_traj.plot(resolver={"t1": 50*tu.ns}) ```
1 parent 0df354c commit 5c96d02

File tree

6 files changed

+471
-49
lines changed

6 files changed

+471
-49
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
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+
"""Folder for Running Analog experiments."""
16+
17+
from cirq_google.experimental.analog_experiments.analog_trajectory_util import (
18+
FrequencyMap as FrequencyMap,
19+
AnalogTrajectory as AnalogTrajectory,
20+
)
Lines changed: 201 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,201 @@
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+
from __future__ import annotations
16+
17+
from typing import AbstractSet, TYPE_CHECKING
18+
19+
import attrs
20+
import matplotlib.pyplot as plt
21+
import numpy as np
22+
import tunits as tu
23+
24+
import cirq
25+
from cirq_google.study import symbol_util as su
26+
27+
if TYPE_CHECKING:
28+
from matplotlib.axes import Axes
29+
30+
31+
@attrs.mutable
32+
class FrequencyMap:
33+
"""Object containing information about the step to a new analog Hamiltonian.
34+
35+
Attributes:
36+
duration: duration of step
37+
qubit_freqs: dict describing qubit frequencies at end of step (None if idle)
38+
couplings: dict describing coupling rates at end of step
39+
"""
40+
41+
duration: su.ValueOrSymbol
42+
qubit_freqs: dict[str, su.ValueOrSymbol | None]
43+
couplings: dict[tuple[str, str], su.ValueOrSymbol]
44+
45+
def _is_parameterized_(self) -> bool:
46+
return (
47+
cirq.is_parameterized(self.duration)
48+
or su.is_parameterized_dict(self.qubit_freqs)
49+
or su.is_parameterized_dict(self.couplings)
50+
)
51+
52+
def _parameter_names_(self) -> AbstractSet[str]:
53+
return (
54+
cirq.parameter_names(self.duration)
55+
| su.dict_param_name(self.qubit_freqs)
56+
| su.dict_param_name(self.couplings)
57+
)
58+
59+
def _resolve_parameters_(
60+
self, resolver: cirq.ParamResolverOrSimilarType, recursive: bool
61+
) -> FrequencyMap:
62+
resolver_ = cirq.ParamResolver(resolver)
63+
return FrequencyMap(
64+
duration=su.direct_symbol_replacement(self.duration, resolver_),
65+
qubit_freqs={
66+
k: su.direct_symbol_replacement(v, resolver_) for k, v in self.qubit_freqs.items()
67+
},
68+
couplings={
69+
k: su.direct_symbol_replacement(v, resolver_) for k, v in self.couplings.items()
70+
},
71+
)
72+
73+
74+
class AnalogTrajectory:
75+
"""Class for handling qubit frequency and coupling trajectories that
76+
define analog experiments. The class is defined using a sparse_trajectory,
77+
which contains time durations of each Hamiltonian ramp element and the
78+
corresponding qubit frequencies and couplings (unassigned qubits and/or
79+
couplers are left unchanged).
80+
"""
81+
82+
def __init__(
83+
self,
84+
*,
85+
full_trajectory: list[FrequencyMap],
86+
qubits: list[str],
87+
pairs: list[tuple[str, str]],
88+
):
89+
self.full_trajectory = full_trajectory
90+
self.qubits = qubits
91+
self.pairs = pairs
92+
93+
@classmethod
94+
def from_sparse_trajectory(
95+
cls,
96+
sparse_trajectory: list[
97+
tuple[
98+
tu.Value,
99+
dict[str, su.ValueOrSymbol | None],
100+
dict[tuple[str, str], su.ValueOrSymbol],
101+
],
102+
],
103+
qubits: list[str] | None = None,
104+
pairs: list[tuple[str, str]] | None = None,
105+
):
106+
"""Construct AnalogTrajectory from sparse trajectory.
107+
108+
Args:
109+
sparse_trajectory: A list of tuples, where each tuple defines a `FrequencyMap`
110+
and contains three elements: (duration, qubit_freqs, coupling_strengths).
111+
`duration` is a tunits value, `qubit_freqs` is a dictionary mapping qubit strings
112+
to detuning frequencies, and `coupling_strengths` is a dictionary mapping qubit
113+
pairs to their coupling strength. This format is considered "sparse" because each
114+
tuple does not need to fully specify all qubits and coupling pairs; any missing
115+
detuning frequency or coupling strength will be set to the same value as the
116+
previous value in the list.
117+
qubits: The qubits in interest. If not provided, automatically parsed from trajectory.
118+
pairs: The pairs in interest. If not provided, automatically parsed from trajectory.
119+
"""
120+
if qubits is None or pairs is None:
121+
qubits_in_traj: list[str] = []
122+
pairs_in_traj: list[tuple[str, str]] = []
123+
for _, q, p in sparse_trajectory:
124+
qubits_in_traj.extend(q.keys())
125+
pairs_in_traj.extend(p.keys())
126+
qubits = list(set(qubits_in_traj))
127+
pairs = list(set(pairs_in_traj))
128+
129+
full_trajectory: list[FrequencyMap] = []
130+
init_qubit_freq_dict: dict[str, tu.Value | None] = {q: None for q in qubits}
131+
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))
133+
134+
for dt, qubit_freq_dict, g_dict in sparse_trajectory:
135+
# If no freq provided, set equal to previous
136+
new_qubit_freq_dict = {
137+
q: qubit_freq_dict.get(q, full_trajectory[-1].qubit_freqs.get(q)) for q in qubits
138+
}
139+
# If no g provided, set equal to previous
140+
new_g_dict: dict[tuple[str, str], tu.Value] = {
141+
p: g_dict.get(p, full_trajectory[-1].couplings.get(p)) for p in pairs # type: ignore[misc]
142+
}
143+
144+
full_trajectory.append(FrequencyMap(dt, new_qubit_freq_dict, new_g_dict))
145+
return cls(full_trajectory=full_trajectory, qubits=qubits, pairs=pairs)
146+
147+
def get_full_trajectory_with_resolved_idles(
148+
self, idle_freq_map: dict[str, tu.Value]
149+
) -> list[FrequencyMap]:
150+
"""Insert idle frequencies instead of None in trajectory."""
151+
152+
resolved_trajectory: list[FrequencyMap] = []
153+
for freq_map in self.full_trajectory:
154+
resolved_qubit_freqs = {
155+
q: idle_freq_map[q] if f is None else f for q, f in freq_map.qubit_freqs.items()
156+
}
157+
resolved_trajectory.append(attrs.evolve(freq_map, qubit_freqs=resolved_qubit_freqs))
158+
return resolved_trajectory
159+
160+
def plot(
161+
self,
162+
idle_freq_map: dict[str, tu.Value] | None = None,
163+
default_idle_freq: tu.Value = 6.5 * tu.GHz,
164+
resolver: cirq.ParamResolverOrSimilarType | None = None,
165+
axes: tuple[Axes, Axes] | None = None,
166+
) -> tuple[Axes, Axes]:
167+
if idle_freq_map is None:
168+
idle_freq_map = {q: default_idle_freq for q in self.qubits}
169+
full_trajectory_resolved = cirq.resolve_parameters(
170+
self.get_full_trajectory_with_resolved_idles(idle_freq_map), resolver
171+
)
172+
unresolved_param_names = set().union(
173+
*[cirq.parameter_names(freq_map) for freq_map in full_trajectory_resolved]
174+
)
175+
if unresolved_param_names:
176+
raise ValueError(f"There are some parameters {unresolved_param_names} not resolved.")
177+
178+
times = np.cumsum([step.duration[tu.ns] for step in full_trajectory_resolved])
179+
180+
if axes is None:
181+
_, axes = plt.subplots(1, 2, figsize=(10, 4))
182+
183+
for qubit_agent in self.qubits:
184+
axes[0].plot(
185+
times,
186+
[step.qubit_freqs[qubit_agent][tu.GHz] for step in full_trajectory_resolved], # type: ignore[index]
187+
label=qubit_agent,
188+
)
189+
for pair_agent in self.pairs:
190+
axes[1].plot(
191+
times,
192+
[step.couplings[pair_agent][tu.MHz] for step in full_trajectory_resolved],
193+
label=pair_agent,
194+
)
195+
196+
for ax, ylabel in zip(axes, ["Qubit freq. (GHz)", "Coupling (MHz)"]):
197+
ax.set_xlabel("Time (ns)")
198+
ax.set_ylabel(ylabel)
199+
ax.legend()
200+
plt.tight_layout()
201+
return axes
Lines changed: 131 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,131 @@
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 pytest
16+
import sympy
17+
import tunits as tu
18+
19+
import cirq
20+
from cirq_google.experimental.analog_experiments import analog_trajectory_util as atu
21+
22+
23+
@pytest.fixture
24+
def freq_map() -> atu.FrequencyMap:
25+
return atu.FrequencyMap(
26+
10 * tu.ns,
27+
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": sympy.Symbol("f_q0_2")},
28+
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): sympy.Symbol("g_q0_1_q0_2")},
29+
)
30+
31+
32+
def test_freq_map_param_names(freq_map: atu.FrequencyMap) -> None:
33+
assert cirq.is_parameterized(freq_map)
34+
assert cirq.parameter_names(freq_map) == {"f_q0_2", "g_q0_1_q0_2"}
35+
36+
37+
def test_freq_map_resolve(freq_map: atu.FrequencyMap) -> None:
38+
resolved_freq_map = cirq.resolve_parameters(
39+
freq_map, {"f_q0_2": 6 * tu.GHz, "g_q0_1_q0_2": 7 * tu.MHz}
40+
)
41+
assert resolved_freq_map == atu.FrequencyMap(
42+
10 * tu.ns,
43+
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 6 * tu.GHz},
44+
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 7 * tu.MHz},
45+
)
46+
47+
48+
FreqMapType = tuple[tu.Value, dict[str, tu.Value | None], dict[tuple[str, str], tu.Value]]
49+
50+
51+
@pytest.fixture
52+
def sparse_trajectory() -> list[FreqMapType]:
53+
traj1: FreqMapType = (20 * tu.ns, {"q0_1": 5 * tu.GHz}, {})
54+
traj2: FreqMapType = (30 * tu.ns, {"q0_2": 8 * tu.GHz}, {})
55+
traj3: FreqMapType = (
56+
40 * tu.ns,
57+
{"q0_0": 8 * tu.GHz, "q0_1": None, "q0_2": None},
58+
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 8 * tu.MHz},
59+
)
60+
return [traj1, traj2, traj3]
61+
62+
63+
def test_full_traj(sparse_trajectory: list[FreqMapType]) -> None:
64+
analog_traj = atu.AnalogTrajectory.from_sparse_trajectory(sparse_trajectory)
65+
assert len(analog_traj.full_trajectory) == 4
66+
assert analog_traj.full_trajectory[0] == atu.FrequencyMap(
67+
0 * tu.ns,
68+
{"q0_0": None, "q0_1": None, "q0_2": None},
69+
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
70+
)
71+
assert analog_traj.full_trajectory[1] == atu.FrequencyMap(
72+
20 * tu.ns,
73+
{"q0_0": None, "q0_1": 5 * tu.GHz, "q0_2": None},
74+
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
75+
)
76+
assert analog_traj.full_trajectory[2] == atu.FrequencyMap(
77+
30 * tu.ns,
78+
{"q0_0": None, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
79+
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
80+
)
81+
assert analog_traj.full_trajectory[3] == atu.FrequencyMap(
82+
40 * tu.ns,
83+
{"q0_0": 8 * tu.GHz, "q0_1": None, "q0_2": None},
84+
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 8 * tu.MHz},
85+
)
86+
87+
88+
def test_get_full_trajectory_with_resolved_idles(sparse_trajectory: list[FreqMapType]) -> None:
89+
90+
analog_traj = atu.AnalogTrajectory.from_sparse_trajectory(sparse_trajectory)
91+
resolved_full_traj = analog_traj.get_full_trajectory_with_resolved_idles(
92+
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 7 * tu.GHz}
93+
)
94+
95+
assert len(resolved_full_traj) == 4
96+
assert resolved_full_traj[0] == atu.FrequencyMap(
97+
0 * tu.ns,
98+
{"q0_0": 5 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 7 * tu.GHz},
99+
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
100+
)
101+
assert resolved_full_traj[1] == atu.FrequencyMap(
102+
20 * tu.ns,
103+
{"q0_0": 5 * tu.GHz, "q0_1": 5 * tu.GHz, "q0_2": 7 * tu.GHz},
104+
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
105+
)
106+
assert resolved_full_traj[2] == atu.FrequencyMap(
107+
30 * tu.ns,
108+
{"q0_0": 5 * tu.GHz, "q0_1": 5 * tu.GHz, "q0_2": 8 * tu.GHz},
109+
{("q0_0", "q0_1"): 0 * tu.MHz, ("q0_1", "q0_2"): 0 * tu.MHz},
110+
)
111+
assert resolved_full_traj[3] == atu.FrequencyMap(
112+
40 * tu.ns,
113+
{"q0_0": 8 * tu.GHz, "q0_1": 6 * tu.GHz, "q0_2": 7 * tu.GHz},
114+
{("q0_0", "q0_1"): 5 * tu.MHz, ("q0_1", "q0_2"): 8 * tu.MHz},
115+
)
116+
117+
118+
def test_plot_with_unresolved_parameters():
119+
traj1: FreqMapType = (20 * tu.ns, {"q0_1": sympy.Symbol("qf")}, {})
120+
traj2: FreqMapType = (sympy.Symbol("t"), {"q0_2": 8 * tu.GHz}, {})
121+
analog_traj = atu.AnalogTrajectory.from_sparse_trajectory([traj1, traj2])
122+
123+
with pytest.raises(ValueError):
124+
analog_traj.plot()
125+
126+
127+
def test_analog_traj_plot():
128+
traj1: FreqMapType = (5 * tu.ns, {"q0_1": sympy.Symbol("qf")}, {("q0_0", "q0_1"): 2 * tu.MHz})
129+
traj2: FreqMapType = (sympy.Symbol("t"), {"q0_2": 8 * tu.GHz}, {})
130+
analog_traj = atu.AnalogTrajectory.from_sparse_trajectory([traj1, traj2])
131+
analog_traj.plot(resolver={"t": 10 * tu.ns, "qf": 5 * tu.GHz})

0 commit comments

Comments
 (0)