|
| 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 |
0 commit comments