Coverage for qml_essentials / unitary.py: 91%

186 statements  

« prev     ^ index     » next       coverage.py v7.13.4, created at 2026-06-11 15:51 +0000

1from typing import Optional, List, Union, Dict, Tuple 

2import itertools 

3import jax.numpy as jnp 

4import jax 

5 

6from qml_essentials import operations as op 

7import logging 

8 

9from qml_essentials.utils import safe_random_split 

10 

11log = logging.getLogger(__name__) 

12 

13 

14# Cache for computed rulers 

15_GOLOMB_RULER_CACHE: Dict[int, Tuple[int, ...]] = {} 

16 

17 

18def _greedy_golomb(d: int) -> Tuple[int, ...]: 

19 """Construct a valid Golomb ruler of order *d* using a greedy algorithm. 

20 

21 Starting from mark 0, each subsequent mark is the smallest integer 

22 whose pairwise differences with all existing marks are distinct. 

23 This always succeeds and produces a valid ruler, though it may not 

24 be optimal (i.e. the max mark may not be minimal). 

25 

26 Args: 

27 d: Order of the ruler (number of marks). 

28 

29 Returns: 

30 Tuple of *d* non-negative integers forming a valid Golomb ruler. 

31 """ 

32 if d <= 0: 

33 return () 

34 marks = [0] 

35 diffs: set = set() 

36 candidate = 1 

37 while len(marks) < d: 

38 new_diffs: set = set() 

39 valid = True 

40 for existing in marks: 

41 diff = candidate - existing 

42 if diff in diffs or diff in new_diffs: 

43 valid = False 

44 break 

45 new_diffs.add(diff) 

46 if valid: 

47 marks.append(candidate) 

48 diffs |= new_diffs 

49 candidate += 1 

50 return tuple(marks) 

51 

52 

53def golomb_ruler(d: int) -> Tuple[int, ...]: 

54 """Return a valid Golomb ruler of order *d*. 

55 

56 A Golomb ruler is a set of *d* non-negative integers such that all 

57 pairwise differences are distinct. When used as the diagonal of a 

58 data-encoding Hamiltonian ``H = diag(marks)``, the resulting Fourier 

59 spectrum ``\\Omega`` has ``|\\Omega| = d(d-1) + 1`` distinct frequencies 

60 with ``|R(k)| = 1`` for all ``k ≠ 0`` — the minimal possible degeneracy 

61 for any *d*-dimensional Hamiltonian. 

62 

63 Uses a greedy construction that always produces a valid ruler. 

64 Results are cached for efficiency. 

65 

66 Args: 

67 d: Order of the ruler (number of marks, equal to the Hilbert 

68 space dimension ``2^n_qubits``). 

69 

70 Returns: 

71 Tuple of *d* non-negative integers forming a Golomb ruler. 

72 

73 Raises: 

74 ValueError: If ``d <= 0``. 

75 

76 References: 

77 Peters et al., "Generalization despite overfitting in quantum 

78 machine learning models", arXiv:2209.05523, Appendix C.4. 

79 """ 

80 if d <= 0: 

81 raise ValueError(f"Golomb ruler order must be positive, got {d}") 

82 if d not in _GOLOMB_RULER_CACHE: 

83 _GOLOMB_RULER_CACHE[d] = _greedy_golomb(d) 

84 return _GOLOMB_RULER_CACHE[d] 

85 

86 

87class UnitaryGates: 

88 """Collection of unitary quantum gates with optional noise simulation.""" 

89 

90 batch_gate_error = True 

91 

92 @staticmethod 

93 def NQubitDepolarizingChannel(p: float, wires: List[int]) -> op.QubitChannel: 

94 """ 

95 Generate Kraus operators for n-qubit depolarizing channel. 

96 

97 The n-qubit depolarizing channel models uniform depolarizing noise 

98 acting on n qubits simultaneously, useful for simulating realistic 

99 multi-qubit noise affecting entangling gates. 

100 

101 Args: 

102 p (float): Total probability of depolarizing error (0 ≤ p ≤ 1). 

103 wires (List[int]): Qubit indices on which the channel acts. 

104 Must contain at least 2 qubits. 

105 

106 Returns: 

107 op.QubitChannel: QubitChannel with Kraus operators 

108 representing the depolarizing noise channel. 

109 

110 Raises: 

111 ValueError: If p is not in [0, 1] or if fewer than 2 qubits provided. 

112 """ 

113 

114 def n_qubit_depolarizing_kraus(p: float, n: int) -> List[jnp.ndarray]: 

115 if not (0.0 <= p <= 1.0): 

116 raise ValueError(f"Probability p must be between 0 and 1, got {p}") 

117 if n < 2: 

118 raise ValueError(f"Number of qubits must be >= 2, got {n}") 

119 

120 Id = jnp.eye(2) 

121 X = op.PauliX._matrix 

122 Y = op.PauliY._matrix 

123 Z = op.PauliZ._matrix 

124 paulis = [Id, X, Y, Z] 

125 

126 dim = 2**n 

127 all_ops = [] 

128 

129 # Generate all n-qubit Pauli tensor products: 

130 for indices in itertools.product(range(4), repeat=n): 

131 P = jnp.eye(1) 

132 for idx in indices: 

133 P = jnp.kron(P, paulis[idx]) 

134 all_ops.append(P) 

135 

136 # Identity operator corresponds to all zeros indices (Id^n) 

137 K0 = jnp.sqrt(1 - p * (4**n - 1) / (4**n)) * jnp.eye(dim) 

138 

139 kraus_ops = [] 

140 for i, P in enumerate(all_ops): 

141 if i == 0: 

142 # Skip the identity, already handled as K0 

143 continue 

144 kraus_ops.append(jnp.sqrt(p / (4**n)) * P) 

145 

146 return [K0] + kraus_ops 

147 

148 return op.QubitChannel(n_qubit_depolarizing_kraus(p, len(wires)), wires=wires) 

149 

150 @staticmethod 

151 def Noise( 

152 wires: Union[int, List[int]], noise_params: Optional[Dict[str, float]] = None 

153 ) -> None: 

154 """ 

155 Apply noise channels to specified qubits. 

156 

157 Applies various single-qubit and multi-qubit noise channels based on 

158 the provided noise parameters dictionary. 

159 

160 Args: 

161 wires (Union[int, List[int]]): Qubit index or list of qubit indices 

162 to apply noise to. 

163 noise_params (Optional[Dict[str, float]]): Dictionary of noise 

164 parameters. Supported keys: 

165 - "BitFlip" (float): Bit flip error probability 

166 - "PhaseFlip" (float): Phase flip error probability 

167 - "Depolarizing" (float): Single-qubit depolarizing probability 

168 - "MultiQubitDepolarizing" (float): Multi-qubit depolarizing 

169 probability (applies if len(wires) > 1) 

170 All parameters default to 0.0 if not provided. 

171 

172 Returns: 

173 None: Noise channels are applied in-place to the circuit. 

174 """ 

175 if noise_params is not None: 

176 if isinstance(wires, int): 

177 wires = [wires] # single qubit gate 

178 

179 # noise on single qubits 

180 for wire in wires: 

181 bf = noise_params.get("BitFlip", 0.0) 

182 if bf > 0: 

183 op.BitFlip(bf, wires=wire) 

184 

185 pf = noise_params.get("PhaseFlip", 0.0) 

186 if pf > 0: 

187 op.PhaseFlip(pf, wires=wire) 

188 

189 dp = noise_params.get("Depolarizing", 0.0) 

190 if dp > 0: 

191 op.DepolarizingChannel(dp, wires=wire) 

192 

193 # noise on two-qubits 

194 if len(wires) > 1: 

195 p = noise_params.get("MultiQubitDepolarizing", 0.0) 

196 if p > 0: 

197 UnitaryGates.NQubitDepolarizingChannel(p, wires) 

198 

199 @staticmethod 

200 def GateError( 

201 w: Union[float, jnp.ndarray, List[float]], 

202 noise_params: Optional[Dict[str, float]] = None, 

203 random_key: Optional[jax.random.PRNGKey] = None, 

204 ) -> Tuple[jnp.ndarray, jax.random.PRNGKey]: 

205 """ 

206 Apply gate error noise to rotation angle(s). 

207 

208 Adds Gaussian noise to gate rotation angles to simulate imperfect 

209 gate implementations. 

210 

211 Args: 

212 w (Union[float, jnp.ndarray, List[float]]): Rotation angle(s) in radians. 

213 noise_params (Optional[Dict[str, float]]): Dictionary with optional 

214 "GateError" key specifying standard deviation of Gaussian noise. 

215 random_key (Optional[jax.random.PRNGKey]): JAX random key for 

216 stochastic noise generation. 

217 

218 Returns: 

219 Tuple[jnp.ndarray, jax.random.PRNGKey]: Tuple containing: 

220 - Modified rotation angle(s) with applied noise 

221 - Updated JAX random key 

222 

223 Raises: 

224 AssertionError: If noise_params contains "GateError" but random_key is None. 

225 """ 

226 if noise_params is not None and noise_params.get("GateError", None) is not None: 

227 assert random_key is not None, ( 

228 "A random_key must be provided when using GateError" 

229 ) 

230 

231 if UnitaryGates.batch_gate_error: 

232 random_key, sub_key = safe_random_split(random_key) 

233 else: 

234 # Use a fixed key so that every batch element (under vmap) 

235 # draws the same noise value, effectively broadcasting. 

236 sub_key = jax.random.key(0) 

237 

238 w += noise_params["GateError"] * jax.random.normal( 

239 sub_key, 

240 ( 

241 w.shape 

242 if isinstance(w, jnp.ndarray) and UnitaryGates.batch_gate_error 

243 else () 

244 ), 

245 ) 

246 return w, random_key 

247 

248 @staticmethod 

249 def Rot( 

250 phi: Union[float, jnp.ndarray, List[float]], 

251 theta: Union[float, jnp.ndarray, List[float]], 

252 omega: Union[float, jnp.ndarray, List[float]], 

253 wires: Union[int, List[int]], 

254 noise_params: Optional[Dict[str, float]] = None, 

255 random_key: Optional[jax.random.PRNGKey] = None, 

256 ) -> None: 

257 """ 

258 Apply general rotation gate with optional noise. 

259 

260 Applies a three-angle rotation Rot(phi, theta, omega) with optional 

261 gate errors and noise channels. 

262 

263 Args: 

264 phi (Union[float, jnp.ndarray, List[float]]): First rotation angle. 

265 theta (Union[float, jnp.ndarray, List[float]]): Second rotation angle. 

266 omega (Union[float, jnp.ndarray, List[float]]): Third rotation angle. 

267 wires (Union[int, List[int]]): Qubit index or indices to apply rotation to. 

268 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

269 Supports BitFlip, PhaseFlip, Depolarizing, and GateError. 

270 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

271 

272 Returns: 

273 None: Gate and noise are applied in-place to the circuit. 

274 """ 

275 if noise_params is not None and "GateError" in noise_params: 

276 phi, random_key = UnitaryGates.GateError(phi, noise_params, random_key) 

277 theta, random_key = UnitaryGates.GateError(theta, noise_params, random_key) 

278 omega, random_key = UnitaryGates.GateError(omega, noise_params, random_key) 

279 op.Rot(phi, theta, omega, wires=wires) 

280 UnitaryGates.Noise(wires, noise_params) 

281 

282 @staticmethod 

283 def PauliRot( 

284 theta: float, 

285 pauli: str, 

286 wires: Union[int, List[int]], 

287 noise_params: Optional[Dict[str, float]] = None, 

288 random_key: Optional[jax.random.PRNGKey] = None, 

289 ) -> None: 

290 """ 

291 Apply general rotation gate with optional noise. 

292 

293 Applies a three-angle rotation Rot(phi, theta, omega) with optional 

294 gate errors and noise channels. 

295 

296 Args: 

297 theta (Union[float, jnp.ndarray, List[float]]): Second rotation angle. 

298 pauli (str): Pauli operator to apply. Must be "X", "Y", or "Z". 

299 wires (Union[int, List[int]]): Qubit index or indices to apply rotation to. 

300 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

301 Supports BitFlip, PhaseFlip, Depolarizing, and GateError. 

302 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

303 

304 Returns: 

305 None: Gate and noise are applied in-place to the circuit. 

306 """ 

307 if noise_params is not None and "GateError" in noise_params: 

308 theta, random_key = UnitaryGates.GateError(theta, noise_params, random_key) 

309 op.PauliRot(theta, pauli, wires=wires) 

310 UnitaryGates.Noise(wires, noise_params) 

311 

312 @staticmethod 

313 def RX( 

314 w: Union[float, jnp.ndarray, List[float]], 

315 wires: Union[int, List[int]], 

316 noise_params: Optional[Dict[str, float]] = None, 

317 random_key: Optional[jax.random.PRNGKey] = None, 

318 ) -> None: 

319 """ 

320 Apply X-axis rotation with optional noise. 

321 

322 Args: 

323 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

324 wires (Union[int, List[int]]): Qubit index or indices. 

325 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

326 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

327 

328 Returns: 

329 None: Gate and noise are applied in-place to the circuit. 

330 """ 

331 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

332 op.RX(w, wires=wires) 

333 UnitaryGates.Noise(wires, noise_params) 

334 

335 @staticmethod 

336 def RY( 

337 w: Union[float, jnp.ndarray, List[float]], 

338 wires: Union[int, List[int]], 

339 noise_params: Optional[Dict[str, float]] = None, 

340 random_key: Optional[jax.random.PRNGKey] = None, 

341 ) -> None: 

342 """ 

343 Apply Y-axis rotation with optional noise. 

344 

345 Args: 

346 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

347 wires (Union[int, List[int]]): Qubit index or indices. 

348 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

349 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

350 

351 Returns: 

352 None: Gate and noise are applied in-place to the circuit. 

353 """ 

354 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

355 op.RY(w, wires=wires) 

356 UnitaryGates.Noise(wires, noise_params) 

357 

358 @staticmethod 

359 def RZ( 

360 w: Union[float, jnp.ndarray, List[float]], 

361 wires: Union[int, List[int]], 

362 noise_params: Optional[Dict[str, float]] = None, 

363 random_key: Optional[jax.random.PRNGKey] = None, 

364 ) -> None: 

365 """ 

366 Apply Z-axis rotation with optional noise. 

367 

368 Args: 

369 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

370 wires (Union[int, List[int]]): Qubit index or indices. 

371 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

372 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

373 

374 Returns: 

375 None: Gate and noise are applied in-place to the circuit. 

376 """ 

377 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

378 op.RZ(w, wires=wires) 

379 UnitaryGates.Noise(wires, noise_params) 

380 

381 @staticmethod 

382 def CRX( 

383 w: Union[float, jnp.ndarray, List[float]], 

384 wires: Union[int, List[int]], 

385 noise_params: Optional[Dict[str, float]] = None, 

386 random_key: Optional[jax.random.PRNGKey] = None, 

387 ) -> None: 

388 """ 

389 Apply controlled X-rotation with optional noise. 

390 

391 Args: 

392 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

393 wires (Union[int, List[int]]): Control and target qubit indices. 

394 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

395 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

396 

397 Returns: 

398 None: Gate and noise are applied in-place to the circuit. 

399 """ 

400 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

401 op.CRX(w, wires=wires) 

402 UnitaryGates.Noise(wires, noise_params) 

403 

404 @staticmethod 

405 def CRY( 

406 w: Union[float, jnp.ndarray, List[float]], 

407 wires: Union[int, List[int]], 

408 noise_params: Optional[Dict[str, float]] = None, 

409 random_key: Optional[jax.random.PRNGKey] = None, 

410 ) -> None: 

411 """ 

412 Apply controlled Y-rotation with optional noise. 

413 

414 Args: 

415 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

416 wires (Union[int, List[int]]): Control and target qubit indices. 

417 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

418 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

419 

420 Returns: 

421 None: Gate and noise are applied in-place to the circuit. 

422 """ 

423 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

424 op.CRY(w, wires=wires) 

425 UnitaryGates.Noise(wires, noise_params) 

426 

427 @staticmethod 

428 def CRZ( 

429 w: Union[float, jnp.ndarray, List[float]], 

430 wires: Union[int, List[int]], 

431 noise_params: Optional[Dict[str, float]] = None, 

432 random_key: Optional[jax.random.PRNGKey] = None, 

433 ) -> None: 

434 """ 

435 Apply controlled Z-rotation with optional noise. 

436 

437 Args: 

438 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

439 wires (Union[int, List[int]]): Control and target qubit indices. 

440 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

441 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

442 

443 Returns: 

444 None: Gate and noise are applied in-place to the circuit. 

445 """ 

446 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

447 op.CRZ(w, wires=wires) 

448 UnitaryGates.Noise(wires, noise_params) 

449 

450 @staticmethod 

451 def RXX( 

452 w: Union[float, jnp.ndarray, List[float]], 

453 wires: Union[int, List[int]], 

454 noise_params: Optional[Dict[str, float]] = None, 

455 random_key: Optional[jax.random.PRNGKey] = None, 

456 ) -> None: 

457 """ 

458 Apply two-qubit XX rotation with optional noise. 

459 

460 Implements ``RXX(theta) = exp(-i theta/2 X ⊗ X)``. 

461 

462 Args: 

463 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

464 wires (Union[int, List[int]]): Two qubit indices. 

465 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

466 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

467 

468 Returns: 

469 None: Gate and noise are applied in-place to the circuit. 

470 """ 

471 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

472 op.RXX(w, wires=wires) 

473 UnitaryGates.Noise(wires, noise_params) 

474 

475 @staticmethod 

476 def RYY( 

477 w: Union[float, jnp.ndarray, List[float]], 

478 wires: Union[int, List[int]], 

479 noise_params: Optional[Dict[str, float]] = None, 

480 random_key: Optional[jax.random.PRNGKey] = None, 

481 ) -> None: 

482 """ 

483 Apply two-qubit YY rotation with optional noise. 

484 

485 Implements ``RYY(theta) = exp(-i theta/2 Y ⊗ Y)``. 

486 

487 Args: 

488 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

489 wires (Union[int, List[int]]): Two qubit indices. 

490 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

491 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

492 

493 Returns: 

494 None: Gate and noise are applied in-place to the circuit. 

495 """ 

496 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

497 op.RYY(w, wires=wires) 

498 UnitaryGates.Noise(wires, noise_params) 

499 

500 @staticmethod 

501 def RZZ( 

502 w: Union[float, jnp.ndarray, List[float]], 

503 wires: Union[int, List[int]], 

504 noise_params: Optional[Dict[str, float]] = None, 

505 random_key: Optional[jax.random.PRNGKey] = None, 

506 ) -> None: 

507 """ 

508 Apply two-qubit ZZ rotation with optional noise. 

509 

510 Implements ``RZZ(theta) = exp(-i theta/2 Z ⊗ Z)``. 

511 

512 Args: 

513 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

514 wires (Union[int, List[int]]): Two qubit indices. 

515 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

516 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

517 

518 Returns: 

519 None: Gate and noise are applied in-place to the circuit. 

520 """ 

521 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

522 op.RZZ(w, wires=wires) 

523 UnitaryGates.Noise(wires, noise_params) 

524 

525 @staticmethod 

526 def RZX( 

527 w: Union[float, jnp.ndarray, List[float]], 

528 wires: Union[int, List[int]], 

529 noise_params: Optional[Dict[str, float]] = None, 

530 random_key: Optional[jax.random.PRNGKey] = None, 

531 ) -> None: 

532 """ 

533 Apply two-qubit ZX rotation with optional noise. 

534 

535 Implements ``RZX(theta) = exp(-i theta/2 Z ⊗ X)``, with ``Z`` acting 

536 on the first wire and ``X`` on the second wire. 

537 

538 Args: 

539 w (Union[float, jnp.ndarray, List[float]]): Rotation angle. 

540 wires (Union[int, List[int]]): Two qubit indices ``[zwire, xwire]``. 

541 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

542 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

543 

544 Returns: 

545 None: Gate and noise are applied in-place to the circuit. 

546 """ 

547 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

548 op.RZX(w, wires=wires) 

549 UnitaryGates.Noise(wires, noise_params) 

550 

551 @staticmethod 

552 def CPhase( 

553 w: Union[float, jnp.ndarray, List[float]], 

554 wires: Union[int, List[int]], 

555 noise_params: Optional[Dict[str, float]] = None, 

556 random_key: Optional[jax.random.PRNGKey] = None, 

557 ) -> None: 

558 """ 

559 Apply controlled phase shift gate with optional noise. 

560 

561 This is a generalization of the CZ gate, applying a phase shift of 

562 exp(i*w) to the |11⟩ state. When w=π, this reduces to CZ. 

563 

564 Args: 

565 w (Union[float, jnp.ndarray, List[float]]): Phase shift angle. 

566 wires (Union[int, List[int]]): Control and target qubit indices. 

567 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

568 random_key (Optional[jax.random.PRNGKey]): JAX random key for noise. 

569 

570 Returns: 

571 None: Gate and noise are applied in-place to the circuit. 

572 """ 

573 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

574 op.ControlledPhaseShift(w, wires=wires) 

575 UnitaryGates.Noise(wires, noise_params) 

576 

577 @staticmethod 

578 def CX( 

579 wires: Union[int, List[int]], 

580 noise_params: Optional[Dict[str, float]] = None, 

581 random_key: Optional[jax.random.PRNGKey] = None, 

582 ) -> None: 

583 """ 

584 Apply controlled-NOT (CNOT) gate with optional noise. 

585 

586 Args: 

587 wires (Union[int, List[int]]): Control and target qubit indices. 

588 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

589 random_key (Optional[jax.random.PRNGKey]): JAX random key for compatibility 

590 (not used in this gate). 

591 

592 Returns: 

593 None: Gate and noise are applied in-place to the circuit. 

594 """ 

595 op.CX(wires=wires) 

596 UnitaryGates.Noise(wires, noise_params) 

597 

598 @staticmethod 

599 def CY( 

600 wires: Union[int, List[int]], 

601 noise_params: Optional[Dict[str, float]] = None, 

602 random_key: Optional[jax.random.PRNGKey] = None, 

603 ) -> None: 

604 """ 

605 Apply controlled-Y gate with optional noise. 

606 

607 Args: 

608 wires (Union[int, List[int]]): Control and target qubit indices. 

609 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

610 random_key (Optional[jax.random.PRNGKey]): JAX random key for compatibility 

611 (not used in this gate). 

612 

613 Returns: 

614 None: Gate and noise are applied in-place to the circuit. 

615 """ 

616 op.CY(wires=wires) 

617 UnitaryGates.Noise(wires, noise_params) 

618 

619 @staticmethod 

620 def CZ( 

621 wires: Union[int, List[int]], 

622 noise_params: Optional[Dict[str, float]] = None, 

623 random_key: Optional[jax.random.PRNGKey] = None, 

624 ) -> None: 

625 """ 

626 Apply controlled-Z gate with optional noise. 

627 

628 Args: 

629 wires (Union[int, List[int]]): Control and target qubit indices. 

630 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

631 random_key (Optional[jax.random.PRNGKey]): JAX random key for compatibility 

632 (not used in this gate). 

633 

634 Returns: 

635 None: Gate and noise are applied in-place to the circuit. 

636 """ 

637 op.CZ(wires=wires) 

638 UnitaryGates.Noise(wires, noise_params) 

639 

640 @staticmethod 

641 def H( 

642 wires: Union[int, List[int]], 

643 noise_params: Optional[Dict[str, float]] = None, 

644 random_key: Optional[jax.random.PRNGKey] = None, 

645 ) -> None: 

646 """ 

647 Apply Hadamard gate with optional noise. 

648 

649 Args: 

650 wires (Union[int, List[int]]): Qubit index or indices. 

651 noise_params (Optional[Dict[str, float]]): Noise parameters dictionary. 

652 random_key (Optional[jax.random.PRNGKey]): JAX random key for compatibility 

653 (not used in this gate). 

654 

655 Returns: 

656 None: Gate and noise are applied in-place to the circuit. 

657 """ 

658 op.H(wires=wires) 

659 UnitaryGates.Noise(wires, noise_params) 

660 

661 @staticmethod 

662 def GolombEncoding( 

663 w: Union[float, jnp.ndarray], 

664 wires: Union[int, List[int]], 

665 noise_params: Optional[Dict[str, float]] = None, 

666 random_key: Optional[jax.random.PRNGKey] = None, 

667 ) -> None: 

668 """Apply Golomb encoding as a diagonal unitary on all given wires. 

669 

670 Implements ``S(x) = exp(-i H x)`` where 

671 ``H = diag(g_0, g_1, ..., g_{d-1})`` and the ``g_j`` are the marks 

672 of a Golomb ruler of order ``d = 2^len(wires)``. This produces a 

673 maximally non-degenerate Fourier spectrum with 

674 ``|\\Omega| = d(d-1) + 1`` distinct frequencies, each with degeneracy 

675 ``|R(k)| = 1``. 

676 

677 See Peters et al., arXiv:2209.05523, Sec. 3.1 and Appendix C.4. 

678 

679 Args: 

680 w: Scalar input value (the data point *x* to encode). 

681 wires: Qubit indices this encoding acts on. All qubits are 

682 acted upon simultaneously via a single multi-qubit diagonal 

683 gate. 

684 noise_params: Optional noise parameters dictionary. 

685 random_key: JAX random key for stochastic noise. 

686 

687 Returns: 

688 None: Gate and noise are applied in-place to the circuit. 

689 """ 

690 wires_list = list(wires) if isinstance(wires, (list, tuple)) else [wires] 

691 d = 2 ** len(wires_list) 

692 marks = jnp.array(golomb_ruler(d), dtype=float) 

693 

694 # Apply gate error to the input angle 

695 w, random_key = UnitaryGates.GateError(w, noise_params, random_key) 

696 

697 # Build diagonal: exp(-i * mark_j * x) 

698 diag = jnp.exp(-1j * marks * w) 

699 

700 op.DiagonalQubitUnitary(diag, wires=wires_list) 

701 UnitaryGates.Noise(wires_list, noise_params)