Skip to content

Commit 75482ad

Browse files
committed
add string representations and update fixtures
1 parent 71dfc5f commit 75482ad

File tree

10 files changed

+447
-89
lines changed

10 files changed

+447
-89
lines changed

README.md

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,6 @@
99

1010
**GADD** is a Python package for empirically optimizing dynamical decoupling (DD) sequences on quantum processors using a genetic algorithm as described in the research paper ["Empirical learning of dynamical decoupling on quantum processors"](https://arxiv.org/abs/2403.02294).
1111

12-
## Key Features
13-
14-
- **Empirical Optimization**: Learn DD sequences directly from quantum hardware feedback
15-
- **Hardware Agnostic**: Works with any quantum backend that supports Qiskit
16-
- **Genetic Algorithm**: Efficient search through large DD sequence spaces
17-
- **Multiple Utility Functions**: Built-in metrics for various quantum applications
18-
- **Comparative Analysis**: Benchmark against standard DD sequences (XY4, CPMG, EDD, URDD)
19-
- **Comprehensive Results**: Detailed training progression and performance analytics
20-
2112
## Installation
2213

2314
### Requirements
@@ -30,6 +21,8 @@ This package is designed to be used with [Qiskit](https://github.com/Qiskit/qisk
3021
pip install gadd
3122
```
3223

24+
To install optional dependencies for developing the package and building documentation, run `pip install gadd[dev]` and `pip install gadd[docs]` respectively.
25+
3326
### Install from Source
3427

3528
```bash

gadd/circuit_padding.py

Lines changed: 64 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from typing import List, Dict, Optional
99
import numpy as np
1010
from qiskit import QuantumCircuit
11+
from qiskit.providers import BackendV2 as Backend
1112
from qiskit.circuit import Instruction, Gate, Delay
1213
from qiskit.circuit.library import IGate, RXGate, RYGate, RZGate
1314
from qiskit.transpiler import InstructionDurations, PassManager
@@ -54,21 +55,53 @@ def __repr__(self):
5455

5556

5657
def get_instruction_duration(
57-
instruction: Instruction, qubits: List[int], unit: str = "dt", dt: float = 1.0
58+
instruction: Instruction,
59+
qubits: List[int],
60+
backend: Optional[Backend] = None,
61+
unit: str = "dt",
62+
dt: float = 0.222e-9,
5863
) -> float:
5964
"""
60-
Get duration of an instruction.
65+
Get duration of an instruction from backend or defaults.
6166
6267
Args:
63-
instruction: The instruction to get duration for
64-
qubits: Qubits the instruction acts on
65-
unit: Time unit ('dt' or 's')
66-
dt: Duration of a single dt in seconds
68+
instruction: The instruction to get duration for.
69+
qubits: Qubits the instruction acts on.
70+
backend: Backend to get durations from.
71+
unit: Time unit ('dt' or 's').
72+
dt: Duration of a single dt in seconds.
6773
6874
Returns:
69-
Duration in specified units
75+
Duration in specified units.
7076
"""
71-
# Default durations in dt units (these should be calibrated for actual backend)
77+
# Handle delay specially
78+
if isinstance(instruction, Delay):
79+
duration = instruction.duration
80+
if instruction.unit == "dt":
81+
return duration if unit == "dt" else duration * dt
82+
else: # assumes 's'
83+
return duration / dt if unit == "dt" else duration
84+
85+
# Try to get from backend first
86+
if backend is not None:
87+
try:
88+
if hasattr(backend, "instruction_durations"):
89+
durations = backend.instruction_durations
90+
else:
91+
durations = InstructionDurations.from_backend(backend)
92+
93+
gate_name = instruction.name.lower()
94+
if len(qubits) == 1:
95+
duration_dt = durations.get(gate_name, qubits[0], "dt")
96+
else:
97+
duration_dt = durations.get(gate_name, qubits, "dt")
98+
99+
if duration_dt is not None:
100+
return duration_dt if unit == "dt" else duration_dt * dt
101+
except:
102+
pass # Fall back to defaults
103+
104+
# Default durations in dt units
72105
default_durations = {
73106
"id": 0, # Identity is virtual
74107
"x": 160, # Single-qubit gate
@@ -79,21 +112,16 @@ def get_instruction_duration(
79112
"rx": 160,
80113
"ry": 160,
81114
"rz": 0, # RZ is virtual
115+
"u1": 0, # U1 is virtual
116+
"u2": 160,
117+
"u3": 160,
82118
"cx": 800, # Two-qubit gate
83119
"cz": 800,
120+
"ecr": 800,
84121
"measure": 4000, # Measurement
85-
"delay": None, # Delay has variable duration
122+
"reset": 1000,
86123
}
87124

88-
# Handle delay specially
89-
if isinstance(instruction, Delay):
90-
duration = instruction.duration
91-
if instruction.unit == "dt":
92-
return duration if unit == "dt" else duration * dt
93-
else: # assumes 's'
94-
return duration / dt if unit == "dt" else duration
95-
96-
# Get duration for standard gates
97125
gate_name = instruction.name.lower()
98126
duration_dt = default_durations.get(gate_name, 160) # Default single-qubit duration
99127

@@ -107,6 +135,7 @@ def apply_dd_strategy(
107135
circuit: QuantumCircuit,
108136
strategy: DDStrategy,
109137
coloring: Dict[int, int],
138+
backend: Optional["Backend"] = None,
110139
instruction_durations: Optional[InstructionDurations] = None,
111140
min_idle_duration: int = 64,
112141
staggered: bool = False,
@@ -118,6 +147,7 @@ def apply_dd_strategy(
118147
circuit: Original quantum circuit.
119148
strategy: DD strategy containing sequences for each color.
120149
coloring: Mapping from qubit to color.
150+
backend: Optional backend for extracting instruction durations.
121151
instruction_durations: Backend-specific instruction durations.
122152
min_idle_duration: Minimum idle duration to insert DD.
123153
staggered: Whether to apply CR-aware staggering for crosstalk suppression.
@@ -134,7 +164,13 @@ def apply_dd_strategy(
134164

135165
# Get default durations if not provided
136166
if instruction_durations is None:
137-
instruction_durations = InstructionDurations()
167+
if backend is not None:
168+
try:
169+
instruction_durations = InstructionDurations.from_backend(backend)
170+
except:
171+
instruction_durations = InstructionDurations()
172+
else:
173+
instruction_durations = InstructionDurations()
138174

139175
# Get unique colors and sort them
140176
unique_colors = sorted(set(coloring.values()))
@@ -179,13 +215,17 @@ def get_staggered_spacing(
179215
continue
180216

181217
# Convert sequence gates to Qiskit gates
218+
# Include identity gates as Delay operations to maintain sequence structure
182219
dd_gates = []
183220
for gate_name in dd_sequence.gates:
184-
if gate_name not in ["I", "Ip", "Im"]: # Skip identity gates
185-
pulse = DDPulse(gate_name, 0, 0) # Qubit and time don't matter here
186-
dd_gates.append(pulse.to_gate())
187-
188-
if not dd_gates:
221+
pulse = DDPulse(gate_name, 0, 0) # Qubit and time don't matter here
222+
dd_gates.append(pulse.to_gate())
223+
224+
if not dd_gates or len(dd_gates) % 2 == 1:
225+
# Skip sequences that don't have even number of gates
226+
print(
227+
f"Warning: Skipping DD sequence for color {color} - odd number of gates ({len(dd_gates)})"
228+
)
189229
continue
190230

191231
# Get spacing for this color if staggered

gadd/gadd.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,22 @@ def from_dict(cls, data: Dict[str, Any]) -> "TrainingConfig":
9696
data["decoupling_group"] = DecouplingGroup(**group_data)
9797
return cls(**data)
9898

99+
def __str__(self) -> str:
100+
"""Return human-readable string representation."""
101+
lines = [
102+
"GADD Training Configuration:",
103+
f" Population size: {self.pop_size}",
104+
f" Sequence length: {self.sequence_length}",
105+
f" Iterations: {self.n_iterations}",
106+
f" Colors: {self.num_colors}",
107+
f" Shots per evaluation: {self.shots}",
108+
f" Mutation probability: {self.mutation_probability}",
109+
f" Mode: {self.mode}",
110+
]
111+
if self.dynamic_mutation:
112+
lines.append(f" Dynamic mutation: enabled (decay={self.mutation_decay})")
113+
return "\n".join(lines)
114+
99115

100116
@dataclass
101117
class TrainingState:
@@ -135,6 +151,34 @@ def to_dict(self) -> Dict[str, Any]:
135151
result_dict["config"] = self.config.to_dict()
136152
return result_dict
137153

154+
def __str__(self) -> str:
155+
"""Return human-readable string representation."""
156+
lines = [
157+
"GADD Training Results:",
158+
f" Best score: {self.best_score:.4f}",
159+
f" Training time: {self.training_time:.1f}s",
160+
f" Iterations completed: {len(self.iteration_data)}",
161+
]
162+
163+
if self.iteration_data:
164+
first_score = self.iteration_data[0]["best_score"]
165+
improvement = self.best_score - first_score
166+
lines.append(f" Score improvement: +{improvement:.4f}")
167+
168+
if self.comparison_data:
169+
lines.append(
170+
f" Compared against {len(self.comparison_data)} standard sequences"
171+
)
172+
best_standard = max(self.comparison_data.values())
173+
advantage = self.best_score - best_standard
174+
if advantage > 0:
175+
lines.append(f" Advantage over best standard: +{advantage:.4f}")
176+
177+
lines.append(f"\nBest sequence found:")
178+
lines.append(f" {self.best_sequence}")
179+
180+
return "\n".join(lines)
181+
138182

139183
class GADD:
140184
"""Genetic Algorithm for Dynamical Decoupling optimization."""
@@ -240,6 +284,8 @@ def apply_dd(
240284
# Use provided backend or fall back to instance backend
241285
backend = backend or self._backend
242286

287+
print("strategy", strategy)
288+
243289
# Get coloring for the circuit
244290
if self._coloring is not None:
245291
coloring_dict = self._coloring.to_dict()
@@ -254,7 +300,10 @@ def apply_dd(
254300
instruction_durations = None
255301
if backend:
256302
try:
257-
instruction_durations = InstructionDurations.from_backend(backend)
303+
if hasattr(backend, "instruction_durations"):
304+
instruction_durations = backend.instruction_durations
305+
else:
306+
instruction_durations = InstructionDurations.from_backend(backend)
258307
except:
259308
pass
260309

@@ -263,6 +312,7 @@ def apply_dd(
263312
target_circuit,
264313
strategy,
265314
coloring_dict,
315+
backend=backend,
266316
instruction_durations=instruction_durations,
267317
staggered=staggered,
268318
)

gadd/strategies.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,9 @@ def __post_init__(self):
7777
if gate not in valid_gates:
7878
raise ValueError(f"Invalid gate: {gate}. Must be one of {valid_gates}")
7979

80+
def __str__(self) -> str:
81+
return "-".join(self.gates)
82+
8083
def __len__(self):
8184
return len(self.gates)
8285

@@ -211,6 +214,19 @@ def get_sequence(self, color: int) -> DDSequence:
211214
def __len__(self) -> int:
212215
return len(self.sequences)
213216

217+
def __str__(self) -> str:
218+
if len(self.sequences) == 1:
219+
# Single sequence case
220+
seq = next(iter(self.sequences.values()))
221+
return f"DD Strategy: {seq}"
222+
else:
223+
# Multi-color case
224+
lines = [f"DD Strategy ({len(self.sequences)} colors):"]
225+
for color in sorted(self.sequences.keys()):
226+
seq = self.sequences[color]
227+
lines.append(f" Color {color}: {seq}")
228+
return "\n".join(lines)
229+
214230
def to_dict(self) -> Dict[str, any]:
215231
"""Convert strategy to dictionary for serialization."""
216232
return {
@@ -271,6 +287,51 @@ def __init__(
271287
# Create reverse mapping for efficiency
272288
self._qubit_to_color = self._color_map.copy()
273289

290+
def __str__(self) -> str:
291+
"""Return human-readable string representation."""
292+
lines = [f"Qubit Coloring ({self.n_colors} colors):"]
293+
for color in sorted(self.assignments.keys()):
294+
qubits = self.assignments[color]
295+
qubit_ranges = self._format_qubit_ranges(qubits)
296+
lines.append(f" Color {color}: {qubit_ranges}")
297+
return "\n".join(lines)
298+
299+
def _format_qubit_ranges(self, qubits: List[int]) -> str:
300+
"""Format qubit list as ranges where possible."""
301+
if not qubits:
302+
return "[]"
303+
304+
sorted_qubits = sorted(qubits)
305+
if len(sorted_qubits) <= 3:
306+
return str(sorted_qubits)
307+
308+
# Try to find consecutive ranges
309+
ranges = []
310+
start = sorted_qubits[0]
311+
end = start
312+
313+
for i in range(1, len(sorted_qubits)):
314+
if sorted_qubits[i] == end + 1:
315+
end = sorted_qubits[i]
316+
else:
317+
if end == start:
318+
ranges.append(str(start))
319+
elif end == start + 1:
320+
ranges.append(f"{start},{end}")
321+
else:
322+
ranges.append(f"{start}-{end}")
323+
start = end = sorted_qubits[i]
324+
325+
# Add final range
326+
if end == start:
327+
ranges.append(str(start))
328+
elif end == start + 1:
329+
ranges.append(f"{start},{end}")
330+
else:
331+
ranges.append(f"{start}-{end}")
332+
333+
return "[" + ",".join(ranges) + "]"
334+
274335
@classmethod
275336
def from_circuit(cls, circuit: QuantumCircuit) -> "ColorAssignment":
276337
"""

gadd/utility_functions.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,10 @@ def compute(self, counts: CountsType) -> float:
148148
def get_name(self) -> str:
149149
return f"Success Probability (|{self._target}⟩)"
150150

151+
def __str__(self) -> str:
152+
"""Return human-readable string representation."""
153+
return f"Success probability for |{self._target}⟩"
154+
151155

152156
class OneNormDistance(UtilityFunction):
153157
"""Utility function based on 1-norm distance to ideal distribution."""
@@ -190,6 +194,17 @@ def compute(self, counts: CountsType) -> float:
190194
def get_name(self) -> str:
191195
return "1-Norm Distance"
192196

197+
def __str__(self) -> str:
198+
"""Return human-readable string representation."""
199+
ideal_states = [
200+
state for state, prob in self.ideal_distribution.items() if prob > 0.01
201+
]
202+
if len(ideal_states) <= 3:
203+
states_str = ", ".join(f"|{state}⟩" for state in ideal_states)
204+
else:
205+
states_str = f"{len(ideal_states)} target states"
206+
return f"1-norm fidelity to {states_str}"
207+
193208

194209
class GHZUtility(OneNormDistance):
195210
"""Specialized utility function for GHZ states."""
@@ -209,6 +224,11 @@ def __init__(self, n_qubits: int):
209224
def get_name(self) -> str:
210225
return "GHZ State Fidelity"
211226

227+
def __str__(self) -> str:
228+
"""Return human-readable string representation."""
229+
n_qubits = len(next(iter(self.ideal_distribution.keys())))
230+
return f"GHZ state fidelity ({n_qubits} qubits)"
231+
212232

213233
class CustomUtility(UtilityFunction):
214234
"""Wrapper for custom utility functions provided by users."""
@@ -234,6 +254,10 @@ def compute(self, counts: CountsType) -> float:
234254
def get_name(self) -> str:
235255
return self._name
236256

257+
def __str__(self) -> str:
258+
"""Return human-readable string representation."""
259+
return f"Custom Utility {self._name}"
260+
237261

238262
class UtilityFactory:
239263
"""Factory class for creating common utility functions."""

0 commit comments

Comments
 (0)