Coverage for tests/test_model.py: 98%

168 statements  

« prev     ^ index     » next       coverage.py v7.6.5, created at 2024-11-15 11:13 +0000

1from qml_essentials.model import Model 

2from qml_essentials.ansaetze import Ansaetze, Circuit 

3import pytest 

4import logging 

5import inspect 

6import shutil 

7import os 

8import hashlib 

9from typing import Optional 

10import pennylane as qml 

11import pennylane.numpy as np 

12 

13logger = logging.getLogger(__name__) 

14 

15 

16@pytest.mark.unittest 

17def test_parameters() -> None: 

18 test_cases = [ 

19 { 

20 "shots": None, 

21 "execution_type": "expval", 

22 "output_qubit": 0, 

23 "force_mean": False, 

24 "exception": False, 

25 }, 

26 { 

27 "shots": None, 

28 "execution_type": "expval", 

29 "output_qubit": -1, 

30 "force_mean": False, 

31 "exception": False, 

32 }, 

33 { 

34 "shots": None, 

35 "execution_type": "expval", 

36 "output_qubit": -1, 

37 "force_mean": True, 

38 "exception": False, 

39 }, 

40 { 

41 "shots": None, 

42 "execution_type": "density", 

43 "output_qubit": 0, 

44 "force_mean": False, 

45 "exception": False, 

46 }, 

47 { 

48 "shots": 1024, 

49 "execution_type": "probs", 

50 "output_qubit": 0, 

51 "force_mean": False, 

52 "exception": False, 

53 }, 

54 { 

55 "shots": 1024, 

56 "execution_type": "probs", 

57 "output_qubit": 0, 

58 "force_mean": True, 

59 "exception": False, 

60 }, 

61 { 

62 "shots": 1024, 

63 "execution_type": "expval", 

64 "output_qubit": 0, 

65 "force_mean": False, 

66 "exception": False, 

67 }, 

68 { 

69 "shots": 1024, 

70 "execution_type": "expval", 

71 "output_qubit": 0, 

72 "force_mean": True, 

73 "exception": False, 

74 }, 

75 { 

76 "shots": 1024, 

77 "execution_type": "density", 

78 "output_qubit": 0, 

79 "force_mean": False, 

80 "exception": True, 

81 }, 

82 ] 

83 

84 # Test the most minimal call 

85 model = Model( 

86 n_qubits=2, 

87 n_layers=1, 

88 circuit_type="Circuit_19", 

89 ) 

90 assert (model() == model(model.params)).all() 

91 

92 for test_case in test_cases: 

93 model = Model( 

94 n_qubits=2, 

95 n_layers=1, 

96 circuit_type="Circuit_19", 

97 output_qubit=test_case["output_qubit"], 

98 shots=test_case["shots"], 

99 ) 

100 

101 if test_case["exception"]: 

102 with pytest.warns(UserWarning): 

103 _ = model( 

104 model.params, 

105 inputs=None, 

106 execution_type=test_case["execution_type"], 

107 force_mean=test_case["force_mean"], 

108 ) 

109 else: 

110 result = model( 

111 model.params, 

112 inputs=None, 

113 execution_type=test_case["execution_type"], 

114 force_mean=test_case["force_mean"], 

115 ) 

116 

117 if test_case["shots"] is None: 

118 assert hasattr( 

119 result, "requires_grad" 

120 ), "No 'requires_grad' property available in output." 

121 else: 

122 # TODO: not supported by PennyLane yet 

123 pass 

124 if test_case["output_qubit"] == -1: 

125 if test_case["force_mean"]: 

126 assert ( 

127 result.shape[0] == 1 

128 ), f"Shape of {test_case['output_qubit']} is not correct." 

129 else: 

130 # check for 2 because of n qubits 

131 assert ( 

132 result.shape[0] == 2 

133 ), f"Shape of {test_case['output_qubit']} is not correct." 

134 str(model) 

135 

136 

137@pytest.mark.unittest 

138def test_cache() -> None: 

139 # Stupid try removing caches 

140 try: 

141 shutil.rmtree(".cache") 

142 except Exception as e: 

143 logger.warning(e) 

144 

145 model = Model( 

146 n_qubits=2, 

147 n_layers=1, 

148 circuit_type="Circuit_19", 

149 ) 

150 

151 result = model( 

152 model.params, 

153 inputs=None, 

154 cache=True, 

155 ) 

156 

157 hs = hashlib.md5( 

158 repr( 

159 { 

160 "n_qubits": model.n_qubits, 

161 "n_layers": model.n_layers, 

162 "pqc": model.pqc.__class__.__name__, 

163 "dru": model.data_reupload, 

164 "params": model.params, 

165 "noise_params": model.noise_params, 

166 "execution_type": model.execution_type, 

167 "inputs": None, 

168 "output_qubit": model.output_qubit, 

169 } 

170 ).encode("utf-8") 

171 ).hexdigest() 

172 

173 cache_folder: str = ".cache" 

174 if not os.path.exists(cache_folder): 

175 raise Exception("Cache folder does not exist.") 

176 

177 name: str = f"pqc_{hs}.npy" 

178 file_path: str = os.path.join(cache_folder, name) 

179 

180 if os.path.isfile(file_path): 

181 cached_result = np.load(file_path) 

182 

183 assert np.array_equal( 

184 result, cached_result 

185 ), "Cached result and calcualted result is not equal." 

186 

187 

188@pytest.mark.expensive 

189@pytest.mark.smoketest 

190def test_lightning() -> None: 

191 model = Model( 

192 n_qubits=12, # model.lightning_threshold 

193 n_layers=1, 

194 circuit_type="Hardware_Efficient", 

195 ) 

196 assert model.circuit.device.name == "lightning.qubit" 

197 

198 _ = model( 

199 model.params, 

200 inputs=None, 

201 ) 

202 

203 

204@pytest.mark.smoketest 

205def test_draw() -> None: 

206 model = Model( 

207 n_qubits=2, 

208 n_layers=1, 

209 circuit_type="Hardware_Efficient", 

210 ) 

211 

212 repr(model) 

213 _ = model.draw(figure=True) 

214 

215 

216@pytest.mark.smoketest 

217def test_initialization() -> None: 

218 test_cases = [ 

219 { 

220 "initialization": "random", 

221 }, 

222 { 

223 "initialization": "zeros", 

224 }, 

225 { 

226 "initialization": "zero-controlled", 

227 }, 

228 { 

229 "initialization": "pi-controlled", 

230 }, 

231 { 

232 "initialization": "pi", 

233 }, 

234 ] 

235 

236 for test_case in test_cases: 

237 model = Model( 

238 n_qubits=2, 

239 n_layers=1, 

240 circuit_type="Circuit_19", 

241 data_reupload=True, 

242 initialization=test_case["initialization"], 

243 output_qubit=0, 

244 shots=1024, 

245 ) 

246 

247 _ = model( 

248 model.params, 

249 inputs=None, 

250 noise_params=None, 

251 cache=False, 

252 execution_type="expval", 

253 ) 

254 

255 

256@pytest.mark.unittest 

257def test_re_initialization() -> None: 

258 model = Model( 

259 n_qubits=2, 

260 n_layers=1, 

261 circuit_type="Circuit_19", 

262 initialization_domain=[-2 * np.pi, 0], 

263 random_seed=1000, 

264 ) 

265 

266 assert model.params.max() <= 0, "Parameters should be in [-2pi, 0]!" 

267 

268 temp_params = model.params.copy() 

269 

270 model.initialize_params(rng=np.random.default_rng(seed=1001)) 

271 

272 assert not np.allclose( 

273 model.params, temp_params, atol=1e-3 

274 ), "Re-Initialization failed!" 

275 

276 

277@pytest.mark.smoketest 

278def test_ansaetze() -> None: 

279 ansatz_cases = Ansaetze.get_available() 

280 

281 for ansatz in ansatz_cases: 

282 # Skipping Circuit_15, as it is not yet correctly implemented 

283 if ansatz.__name__ == "Circuit_15": 

284 continue 

285 

286 logger.info(f"Testing Ansatz: {ansatz.__name__}") 

287 model = Model( 

288 n_qubits=4, 

289 n_layers=1, 

290 circuit_type=ansatz.__name__, 

291 data_reupload=True, 

292 initialization="random", 

293 output_qubit=0, 

294 shots=1024, 

295 ) 

296 

297 _ = model( 

298 model.params, 

299 inputs=None, 

300 noise_params=None, 

301 cache=False, 

302 execution_type="expval", 

303 ) 

304 

305 class custom_ansatz(Circuit): 

306 @staticmethod 

307 def n_params_per_layer(n_qubits: int) -> int: 

308 return n_qubits * 3 

309 

310 @staticmethod 

311 def get_control_indices(n_qubits: int) -> Optional[np.ndarray]: 

312 return None 

313 

314 @staticmethod 

315 def build(w: np.ndarray, n_qubits: int): 

316 w_idx = 0 

317 for q in range(n_qubits): 

318 qml.RY(w[w_idx], wires=q) 

319 w_idx += 1 

320 qml.RZ(w[w_idx], wires=q) 

321 w_idx += 1 

322 

323 if n_qubits > 1: 

324 for q in range(n_qubits - 1): 

325 qml.CZ(wires=[q, q + 1]) 

326 

327 model = Model( 

328 n_qubits=2, 

329 n_layers=1, 

330 circuit_type=custom_ansatz, 

331 data_reupload=True, 

332 initialization="random", 

333 output_qubit=0, 

334 shots=1024, 

335 ) 

336 logger.info(f"{str(model)}") 

337 

338 _ = model( 

339 model.params, 

340 inputs=None, 

341 noise_params=None, 

342 cache=False, 

343 execution_type="expval", 

344 ) 

345 

346 

347@pytest.mark.unittest 

348def test_available_ansaetze() -> None: 

349 ansatze = set(Ansaetze.get_available()) 

350 

351 actual_ansaetze = set( 

352 ansatz for ansatz in Ansaetze.__dict__.values() if inspect.isclass(ansatz) 

353 ) 

354 # check that the classes are the ones returned by .__subclasses__ 

355 assert actual_ansaetze == ansatze 

356 

357 

358@pytest.mark.unittest 

359def test_multi_input() -> None: 

360 input_cases = [ 

361 np.random.rand(1), 

362 np.random.rand(1, 1), 

363 np.random.rand(1, 2), 

364 np.random.rand(1, 3), 

365 np.random.rand(2, 1), 

366 np.random.rand(20, 1), 

367 ] 

368 input_cases = [2 * np.pi * i for i in input_cases] 

369 input_cases.append(None) 

370 

371 for inputs in input_cases: 

372 logger.info( 

373 f"Testing input with shape: " 

374 f"{inputs.shape if inputs is not None else 'None'}" 

375 ) 

376 model = Model( 

377 n_qubits=2, 

378 n_layers=1, 

379 circuit_type="Circuit_19", 

380 data_reupload=True, 

381 initialization="random", 

382 output_qubit=0, 

383 shots=1024, 

384 ) 

385 

386 out = model( 

387 model.params, 

388 inputs=inputs, 

389 noise_params=None, 

390 cache=False, 

391 execution_type="expval", 

392 ) 

393 

394 if inputs is not None: 

395 if len(out.shape) > 0: 

396 assert out.shape[0] == inputs.shape[0], ( 

397 f"batch dimension mismatch, expected {inputs.shape[0]} " 

398 f"as an output dimension, but got {out.shape[0]}" 

399 ) 

400 else: 

401 assert ( 

402 inputs.shape[0] == 1 

403 ), "expected one elemental input for zero dimensional output" 

404 else: 

405 assert len(out.shape) == 0, "expected one elemental output for empty input" 

406 

407 

408@pytest.mark.smoketest 

409def test_dru() -> None: 

410 dru_cases = [False, True] 

411 

412 for dru in dru_cases: 

413 model = Model( 

414 n_qubits=2, 

415 n_layers=1, 

416 circuit_type="Circuit_19", 

417 data_reupload=dru, 

418 initialization="random", 

419 output_qubit=0, 

420 shots=1024, 

421 ) 

422 

423 _ = model( 

424 model.params, 

425 inputs=None, 

426 noise_params=None, 

427 cache=False, 

428 execution_type="expval", 

429 ) 

430 

431 

432@pytest.mark.unittest 

433def test_local_state() -> None: 

434 test_cases = [ 

435 { 

436 "noise_params": None, 

437 "execution_type": "density", 

438 }, 

439 { 

440 "noise_params": { 

441 "BitFlip": 0.1, 

442 "PhaseFlip": 0.2, 

443 "AmplitudeDamping": 0.3, 

444 "PhaseDamping": 0.4, 

445 "DepolarizingChannel": 0.5, 

446 }, 

447 "execution_type": "density", 

448 }, 

449 { 

450 "noise_params": None, 

451 "execution_type": "expval", 

452 }, 

453 ] 

454 

455 model = Model( 

456 n_qubits=2, 

457 n_layers=1, 

458 circuit_type="Circuit_19", 

459 data_reupload=True, 

460 initialization="random", 

461 output_qubit=0, 

462 ) 

463 

464 # Check default values 

465 assert model.noise_params is None 

466 assert model.execution_type == "expval" 

467 

468 for test_case in test_cases: 

469 model = Model( 

470 n_qubits=2, 

471 n_layers=1, 

472 circuit_type="Circuit_19", 

473 data_reupload=True, 

474 initialization="random", 

475 output_qubit=0, 

476 ) 

477 

478 model.noise_params = test_case["noise_params"] 

479 model.execution_type = test_case["execution_type"] 

480 

481 _ = model( 

482 model.params, 

483 inputs=None, 

484 noise_params=None, 

485 cache=False, 

486 ) 

487 

488 # check if setting "externally" is working 

489 assert model.noise_params == test_case["noise_params"] 

490 assert model.execution_type == test_case["execution_type"] 

491 

492 model = Model( 

493 n_qubits=2, 

494 n_layers=1, 

495 circuit_type="Circuit_19", 

496 data_reupload=True, 

497 initialization="random", 

498 output_qubit=0, 

499 ) 

500 

501 _ = model( 

502 model.params, 

503 inputs=None, 

504 cache=False, 

505 noise_params=test_case["noise_params"], 

506 execution_type=test_case["execution_type"], 

507 ) 

508 

509 # check if setting in the forward call is working 

510 assert model.noise_params == test_case["noise_params"] 

511 assert model.execution_type == test_case["execution_type"] 

512 

513 

514@pytest.mark.unittest 

515def test_local_and_global_meas() -> None: 

516 test_cases = [ 

517 { 

518 "inputs": None, 

519 "execution_type": "expval", 

520 "output_qubit": -1, 

521 "shots": None, 

522 "out_shape": (2, 1), 

523 "warning": False, 

524 }, 

525 { 

526 "inputs": np.array([0.1, 0.2, 0.3]), 

527 "execution_type": "expval", 

528 "output_qubit": -1, 

529 "shots": None, 

530 "out_shape": (2, 3), 

531 "warning": False, 

532 }, 

533 { 

534 "inputs": np.array([0.1, 0.2, 0.3]), 

535 "execution_type": "expval", 

536 "output_qubit": 0, 

537 "shots": None, 

538 "out_shape": (3,), 

539 "warning": False, 

540 }, 

541 { 

542 "inputs": np.array([0.1, 0.2, 0.3]), 

543 "execution_type": "expval", 

544 "output_qubit": [0, 1], 

545 "shots": None, 

546 "out_shape": (3,), 

547 "warning": False, 

548 }, 

549 { 

550 "inputs": None, 

551 "execution_type": "density", 

552 "output_qubit": -1, 

553 "shots": None, 

554 "out_shape": (4, 4), 

555 "warning": False, 

556 }, 

557 { 

558 "inputs": np.array([0.1, 0.2, 0.3]), 

559 "execution_type": "density", 

560 "output_qubit": -1, 

561 "shots": None, 

562 "out_shape": (3, 4, 4), 

563 "warning": False, 

564 }, 

565 { 

566 "inputs": np.array([0.1, 0.2, 0.3]), 

567 "execution_type": "density", 

568 "output_qubit": 0, 

569 "shots": None, 

570 "out_shape": (3, 4, 4), 

571 "warning": True, 

572 }, 

573 { 

574 "inputs": np.array([0.1, 0.2, 0.3]), 

575 "execution_type": "probs", 

576 "output_qubit": -1, 

577 "shots": 1024, 

578 "out_shape": (3, 4), 

579 "warning": False, 

580 }, 

581 { 

582 "inputs": np.array([0.1, 0.2, 0.3]), 

583 "execution_type": "probs", 

584 "output_qubit": 0, 

585 "shots": 1024, 

586 "out_shape": (3, 2), 

587 "warning": False, 

588 }, 

589 { 

590 "inputs": np.array([0.1, 0.2, 0.3]), 

591 "execution_type": "probs", 

592 "output_qubit": [0, 1], 

593 "shots": 1024, 

594 "out_shape": (3, 4), 

595 "warning": False, 

596 }, 

597 ] 

598 

599 for test_case in test_cases: 

600 model = Model( 

601 n_qubits=2, 

602 n_layers=1, 

603 circuit_type="Circuit_19", 

604 data_reupload=True, 

605 initialization="random", 

606 output_qubit=test_case["output_qubit"], 

607 shots=test_case["shots"], 

608 ) 

609 if test_case["warning"]: 

610 with pytest.warns(UserWarning): 

611 out = model( 

612 model.params, 

613 inputs=test_case["inputs"], 

614 noise_params=None, 

615 cache=False, 

616 execution_type=test_case["execution_type"], 

617 ) 

618 else: 

619 out = model( 

620 model.params, 

621 inputs=test_case["inputs"], 

622 noise_params=None, 

623 cache=False, 

624 execution_type=test_case["execution_type"], 

625 ) 

626 

627 assert ( 

628 out.shape == test_case["out_shape"] 

629 ), f"Expected {test_case['out_shape']}, got shape {out.shape}\ 

630 for test case {test_case}" 

631 

632 

633@pytest.mark.unittest 

634def test_parity() -> None: 

635 model_a = Model( 

636 n_qubits=2, 

637 n_layers=1, 

638 circuit_type="Circuit_1", 

639 output_qubit=[0, 1], # parity 

640 ) 

641 model_b = Model( 

642 n_qubits=2, 

643 n_layers=1, 

644 circuit_type="Circuit_1", 

645 output_qubit=-1, # individual 

646 ) 

647 

648 result_a = model_a(params=model_a.params, inputs=None, force_mean=True) 

649 result_b = model_b( 

650 params=model_a.params, inputs=None, force_mean=True 

651 ) # use same params! 

652 

653 assert not np.allclose( 

654 result_a, result_b 

655 ), f"Models should be different! Got {result_a} and {result_b}" 

656 

657 

658@pytest.mark.smoketest 

659def test_params_store() -> None: 

660 model = Model( 

661 n_qubits=2, 

662 n_layers=1, 

663 circuit_type="Circuit_1", 

664 ) 

665 opt = qml.AdamOptimizer(stepsize=0.01) 

666 

667 def cost(params): 

668 return model(params=params, inputs=np.array([0])).mean()._value 

669 

670 params, cost = opt.step_and_cost(cost, model.params)