Coverage for tests/test_model.py: 94%

189 statements  

« prev     ^ index     » next       coverage.py v7.6.12, created at 2025-03-07 14:54 +0000

1from qml_essentials.model import Model 

2from qml_essentials.ansaetze import Ansaetze, Circuit, Gates 

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 

13from typing import List, Callable 

14 

15logger = logging.getLogger(__name__) 

16 

17 

18@pytest.mark.unittest 

19def test_parameters() -> None: 

20 test_cases = [ 

21 # { 

22 # "shots": None, 

23 # "execution_type": "expval", 

24 # "output_qubit": 0, 

25 # "force_mean": False, 

26 # "exception": False, 

27 # }, 

28 # { 

29 # "shots": None, 

30 # "execution_type": "expval", 

31 # "output_qubit": -1, 

32 # "force_mean": False, 

33 # "exception": False, 

34 # }, 

35 # { 

36 # "shots": None, 

37 # "execution_type": "expval", 

38 # "output_qubit": -1, 

39 # "force_mean": True, 

40 # "exception": False, 

41 # }, 

42 # { 

43 # "shots": None, 

44 # "execution_type": "density", 

45 # "output_qubit": 0, 

46 # "force_mean": False, 

47 # "exception": False, 

48 # }, 

49 # { 

50 # "shots": 1024, 

51 # "execution_type": "probs", 

52 # "output_qubit": 0, 

53 # "force_mean": False, 

54 # "exception": False, 

55 # }, 

56 { 

57 "shots": 1024, 

58 "execution_type": "probs", 

59 "output_qubit": -1, 

60 "force_mean": True, 

61 "exception": False, 

62 }, 

63 { 

64 "shots": 1024, 

65 "execution_type": "expval", 

66 "output_qubit": 0, 

67 "force_mean": False, 

68 "exception": False, 

69 }, 

70 { 

71 "shots": 1024, 

72 "execution_type": "expval", 

73 "output_qubit": 0, 

74 "force_mean": True, 

75 "exception": False, 

76 }, 

77 { 

78 "shots": 1024, 

79 "execution_type": "density", 

80 "output_qubit": 0, 

81 "force_mean": False, 

82 "exception": True, 

83 }, 

84 ] 

85 

86 # Test the most minimal call 

87 model = Model( 

88 n_qubits=2, 

89 n_layers=1, 

90 circuit_type="Circuit_19", 

91 ) 

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

93 

94 for test_case in test_cases: 

95 model = Model( 

96 n_qubits=2, 

97 n_layers=1, 

98 circuit_type="Circuit_19", 

99 output_qubit=test_case["output_qubit"], 

100 shots=test_case["shots"], 

101 ) 

102 

103 if test_case["exception"]: 

104 with pytest.warns(UserWarning): 

105 _ = model( 

106 model.params, 

107 inputs=None, 

108 execution_type=test_case["execution_type"], 

109 force_mean=test_case["force_mean"], 

110 ) 

111 else: 

112 result = model( 

113 model.params, 

114 inputs=None, 

115 execution_type=test_case["execution_type"], 

116 force_mean=test_case["force_mean"], 

117 ) 

118 

119 if test_case["shots"] is None: 

120 assert hasattr( 

121 result, "requires_grad" 

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

123 else: 

124 # TODO: not supported by PennyLane yet 

125 pass 

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

127 if test_case["force_mean"]: 

128 assert ( 

129 result.size == 1 or result.shape[0] == 1 

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

131 else: 

132 if test_case["execution_type"] == "expval": 

133 # check for 2 because of n qubits 

134 assert ( 

135 result.shape[0] == 2 

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

137 elif test_case["execution_type"] == "probs": 

138 assert ( 

139 result.shape[0] == 4 

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

141 str(model) 

142 

143 

144@pytest.mark.smoketest 

145def test_encoding() -> None: 

146 test_cases = [ 

147 {"encoding_unitary": Gates.RX, "type": Callable, "input": [0]}, 

148 { 

149 "encoding_unitary": [Gates.RX, Gates.RY], 

150 "type": List, 

151 "input": [[0, 0]], 

152 }, 

153 {"encoding_unitary": "RX", "type": Callable, "input": [0]}, 

154 {"encoding_unitary": ["RX", "RY"], "type": List, "input": [[0, 0]]}, 

155 ] 

156 

157 for test_case in test_cases: 

158 model = Model( 

159 n_qubits=2, 

160 n_layers=1, 

161 circuit_type="Circuit_19", 

162 encoding=test_case["encoding_unitary"], 

163 ) 

164 _ = model( 

165 model.params, 

166 inputs=test_case["input"], 

167 ) 

168 

169 assert isinstance(model._enc, test_case["type"]) 

170 

171 

172@pytest.mark.unittest 

173def test_cache() -> None: 

174 # Stupid try removing caches 

175 try: 

176 shutil.rmtree(".cache") 

177 except Exception as e: 

178 logger.warning(e) 

179 

180 model = Model( 

181 n_qubits=2, 

182 n_layers=1, 

183 circuit_type="Circuit_19", 

184 ) 

185 

186 result = model( 

187 model.params, 

188 inputs=None, 

189 cache=True, 

190 ) 

191 

192 hs = hashlib.md5( 

193 repr( 

194 { 

195 "n_qubits": model.n_qubits, 

196 "n_layers": model.n_layers, 

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

198 "dru": model.data_reupload, 

199 "params": model.params, 

200 "noise_params": model.noise_params, 

201 "execution_type": model.execution_type, 

202 "inputs": np.array([[0]]), 

203 "output_qubit": model.output_qubit, 

204 } 

205 ).encode("utf-8") 

206 ).hexdigest() 

207 

208 cache_folder: str = ".cache" 

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

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

211 

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

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

214 

215 if os.path.isfile(file_path): 

216 cached_result = np.load(file_path) 

217 

218 assert np.array_equal( 

219 result, cached_result 

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

221 

222 

223@pytest.mark.expensive 

224@pytest.mark.smoketest 

225def test_lightning() -> None: 

226 model = Model( 

227 n_qubits=12, # model.lightning_threshold 

228 n_layers=1, 

229 circuit_type="Hardware_Efficient", 

230 ) 

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

232 

233 _ = model( 

234 model.params, 

235 inputs=None, 

236 ) 

237 

238 

239@pytest.mark.smoketest 

240def test_draw() -> None: 

241 model = Model( 

242 n_qubits=2, 

243 n_layers=1, 

244 circuit_type="Hardware_Efficient", 

245 ) 

246 

247 repr(model) 

248 _ = model.draw(figure=True) 

249 

250 

251@pytest.mark.smoketest 

252def test_initialization() -> None: 

253 test_cases = [ 

254 { 

255 "initialization": "random", 

256 }, 

257 { 

258 "initialization": "zeros", 

259 }, 

260 { 

261 "initialization": "zero-controlled", 

262 }, 

263 { 

264 "initialization": "pi-controlled", 

265 }, 

266 { 

267 "initialization": "pi", 

268 }, 

269 ] 

270 

271 for test_case in test_cases: 

272 model = Model( 

273 n_qubits=2, 

274 n_layers=1, 

275 circuit_type="Circuit_19", 

276 data_reupload=True, 

277 initialization=test_case["initialization"], 

278 output_qubit=0, 

279 shots=1024, 

280 ) 

281 

282 _ = model( 

283 model.params, 

284 inputs=None, 

285 noise_params=None, 

286 cache=False, 

287 execution_type="expval", 

288 ) 

289 

290 

291@pytest.mark.unittest 

292def test_re_initialization() -> None: 

293 model = Model( 

294 n_qubits=2, 

295 n_layers=1, 

296 circuit_type="Circuit_19", 

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

298 random_seed=1000, 

299 ) 

300 

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

302 

303 temp_params = model.params.copy() 

304 

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

306 

307 assert not np.allclose( 

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

309 ), "Re-Initialization failed!" 

310 

311 

312@pytest.mark.smoketest 

313def test_ansaetze() -> None: 

314 ansatz_cases = Ansaetze.get_available() 

315 

316 for ansatz in ansatz_cases: 

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

318 model = Model( 

319 n_qubits=4, 

320 n_layers=1, 

321 circuit_type=ansatz.__name__, 

322 data_reupload=False, 

323 initialization="random", 

324 output_qubit=0, 

325 shots=1024, 

326 ) 

327 

328 _ = model( 

329 model.params, 

330 inputs=None, 

331 noise_params={ 

332 "GateError": 0.1, 

333 "BitFlip": 0.1, 

334 "PhaseFlip": 0.2, 

335 "AmplitudeDamping": 0.3, 

336 "PhaseDamping": 0.4, 

337 "Depolarizing": 0.5, 

338 "ThermalRelaxation": {"T1": 2000.0, "T2": 1000.0, "t_factor": 1}, 

339 "StatePreparation": 0.1, 

340 "Measurement": 0.1, 

341 }, 

342 cache=False, 

343 execution_type="density", 

344 ) 

345 

346 class custom_ansatz(Circuit): 

347 @staticmethod 

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

349 return n_qubits * 3 

350 

351 @staticmethod 

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

353 return None 

354 

355 @staticmethod 

356 def build(w: np.ndarray, n_qubits: int, noise_params=None): 

357 w_idx = 0 

358 for q in range(n_qubits): 

359 Gates.RY(w[w_idx], wires=q, noise_params=noise_params) 

360 w_idx += 1 

361 Gates.RZ(w[w_idx], wires=q, noise_params=noise_params) 

362 w_idx += 1 

363 

364 if n_qubits > 1: 

365 for q in range(n_qubits - 1): 

366 Gates.CZ(wires=[q, q + 1], noise_params=noise_params) 

367 

368 model = Model( 

369 n_qubits=2, 

370 n_layers=1, 

371 circuit_type=custom_ansatz, 

372 data_reupload=True, 

373 initialization="random", 

374 output_qubit=0, 

375 shots=1024, 

376 ) 

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

378 

379 _ = model( 

380 model.params, 

381 inputs=None, 

382 noise_params={ 

383 "GateError": 0.1, 

384 "PhaseFlip": 0.2, 

385 "AmplitudeDamping": 0.3, 

386 "Depolarizing": 0.5, 

387 }, 

388 cache=False, 

389 execution_type="density", 

390 ) 

391 

392 with pytest.warns(UserWarning): 

393 _ = model( 

394 model.params, 

395 inputs=None, 

396 noise_params={ 

397 "UnsupportedNoise": 0.1, 

398 }, 

399 cache=False, 

400 execution_type="density", 

401 ) 

402 

403 

404@pytest.mark.unittest 

405def test_available_ansaetze() -> None: 

406 ansatze = set(Ansaetze.get_available()) 

407 

408 actual_ansaetze = set( 

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

410 ) 

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

412 assert actual_ansaetze == ansatze 

413 

414 

415@pytest.mark.unittest 

416def test_multi_input() -> None: 

417 input_cases = [ 

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

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

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

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

422 np.random.rand(3, 2), 

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

424 ] 

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

426 input_cases.append(None) 

427 

428 for inputs in input_cases: 

429 logger.info( 

430 f"Testing input with shape: " 

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

432 ) 

433 encoding = ( 

434 Gates.RX if inputs is None else [Gates.RX for _ in range(inputs.shape[1])] 

435 ) 

436 model = Model( 

437 n_qubits=2, 

438 n_layers=1, 

439 circuit_type="Circuit_19", 

440 data_reupload=True, 

441 initialization="random", 

442 encoding=encoding, 

443 output_qubit=0, 

444 shots=1024, 

445 ) 

446 

447 out = model( 

448 model.params, 

449 inputs=inputs, 

450 noise_params=None, 

451 cache=False, 

452 execution_type="expval", 

453 ) 

454 

455 if inputs is not None: 

456 if len(out.shape) > 0: 

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

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

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

460 ) 

461 else: 

462 assert ( 

463 inputs.shape[0] == 1 

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

465 else: 

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

467 

468 

469@pytest.mark.smoketest 

470def test_dru() -> None: 

471 dru_cases = [False, True] 

472 

473 for dru in dru_cases: 

474 model = Model( 

475 n_qubits=2, 

476 n_layers=1, 

477 circuit_type="Circuit_19", 

478 data_reupload=dru, 

479 initialization="random", 

480 output_qubit=0, 

481 shots=1024, 

482 ) 

483 

484 _ = model( 

485 model.params, 

486 inputs=None, 

487 noise_params=None, 

488 cache=False, 

489 execution_type="expval", 

490 ) 

491 

492 

493@pytest.mark.unittest 

494def test_local_state() -> None: 

495 test_cases = [ 

496 { 

497 "noise_params": None, 

498 "execution_type": "density", 

499 }, 

500 { 

501 "noise_params": { 

502 "BitFlip": 0.1, 

503 "PhaseFlip": 0.2, 

504 "AmplitudeDamping": 0.3, 

505 "PhaseDamping": 0.4, 

506 "Depolarizing": 0.5, 

507 }, 

508 "execution_type": "density", 

509 }, 

510 { 

511 "noise_params": None, 

512 "execution_type": "expval", 

513 }, 

514 ] 

515 

516 model = Model( 

517 n_qubits=2, 

518 n_layers=1, 

519 circuit_type="Circuit_19", 

520 data_reupload=True, 

521 initialization="random", 

522 output_qubit=0, 

523 ) 

524 

525 # Check default values 

526 assert model.noise_params is None 

527 assert model.execution_type == "expval" 

528 

529 for test_case in test_cases: 

530 model = Model( 

531 n_qubits=2, 

532 n_layers=1, 

533 circuit_type="Circuit_19", 

534 data_reupload=True, 

535 initialization="random", 

536 output_qubit=0, 

537 ) 

538 

539 model.noise_params = test_case["noise_params"] 

540 model.execution_type = test_case["execution_type"] 

541 

542 _ = model( 

543 model.params, 

544 inputs=None, 

545 noise_params=None, 

546 cache=False, 

547 ) 

548 

549 # check if setting "externally" is working 

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

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

552 

553 model = Model( 

554 n_qubits=2, 

555 n_layers=1, 

556 circuit_type="Circuit_19", 

557 data_reupload=True, 

558 initialization="random", 

559 output_qubit=0, 

560 ) 

561 

562 _ = model( 

563 model.params, 

564 inputs=None, 

565 cache=False, 

566 noise_params=test_case["noise_params"], 

567 execution_type=test_case["execution_type"], 

568 ) 

569 

570 # check if setting in the forward call is working 

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

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

573 

574 

575@pytest.mark.unittest 

576def test_local_and_global_meas() -> None: 

577 test_cases = [ 

578 { 

579 "inputs": None, 

580 "execution_type": "expval", 

581 "output_qubit": -1, 

582 "shots": None, 

583 "out_shape": (2,), 

584 "warning": False, 

585 }, 

586 { 

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

588 "execution_type": "expval", 

589 "output_qubit": -1, 

590 "shots": None, 

591 "out_shape": (2, 3), 

592 "warning": False, 

593 }, 

594 { 

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

596 "execution_type": "expval", 

597 "output_qubit": 0, 

598 "shots": None, 

599 "out_shape": (3,), 

600 "warning": False, 

601 }, 

602 { 

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

604 "execution_type": "expval", 

605 "output_qubit": [0, 1], 

606 "shots": None, 

607 "out_shape": (3,), 

608 "warning": False, 

609 }, 

610 { 

611 "inputs": None, 

612 "execution_type": "density", 

613 "output_qubit": -1, 

614 "shots": None, 

615 "out_shape": (4, 4), 

616 "warning": False, 

617 }, 

618 { 

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

620 "execution_type": "density", 

621 "output_qubit": -1, 

622 "shots": None, 

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

624 "warning": False, 

625 }, 

626 { 

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

628 "execution_type": "density", 

629 "output_qubit": 0, 

630 "shots": None, 

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

632 "warning": True, 

633 }, 

634 { 

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

636 "execution_type": "probs", 

637 "output_qubit": -1, 

638 "shots": 1024, 

639 "out_shape": (3, 4), 

640 "warning": False, 

641 }, 

642 { 

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

644 "execution_type": "probs", 

645 "output_qubit": 0, 

646 "shots": 1024, 

647 "out_shape": (3, 2), 

648 "warning": False, 

649 }, 

650 { 

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

652 "execution_type": "probs", 

653 "output_qubit": [0, 1], 

654 "shots": 1024, 

655 "out_shape": (3, 4), 

656 "warning": False, 

657 }, 

658 ] 

659 

660 for test_case in test_cases: 

661 model = Model( 

662 n_qubits=2, 

663 n_layers=1, 

664 circuit_type="Circuit_19", 

665 data_reupload=True, 

666 initialization="random", 

667 output_qubit=test_case["output_qubit"], 

668 shots=test_case["shots"], 

669 ) 

670 if test_case["warning"]: 

671 with pytest.warns(UserWarning): 

672 out = model( 

673 model.params, 

674 inputs=test_case["inputs"], 

675 noise_params=None, 

676 cache=False, 

677 execution_type=test_case["execution_type"], 

678 ) 

679 else: 

680 out = model( 

681 model.params, 

682 inputs=test_case["inputs"], 

683 noise_params=None, 

684 cache=False, 

685 execution_type=test_case["execution_type"], 

686 ) 

687 

688 assert ( 

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

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

691 for test case {test_case}" 

692 

693 

694@pytest.mark.unittest 

695def test_parity() -> None: 

696 model_a = Model( 

697 n_qubits=2, 

698 n_layers=1, 

699 circuit_type="Circuit_1", 

700 output_qubit=[0, 1], # parity 

701 ) 

702 model_b = Model( 

703 n_qubits=2, 

704 n_layers=1, 

705 circuit_type="Circuit_1", 

706 output_qubit=-1, # individual 

707 ) 

708 

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

710 result_b = model_b( 

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

712 ) # use same params! 

713 

714 assert not np.allclose( 

715 result_a, result_b 

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

717 

718 

719@pytest.mark.smoketest 

720def test_params_store() -> None: 

721 model = Model( 

722 n_qubits=2, 

723 n_layers=1, 

724 circuit_type="Circuit_1", 

725 ) 

726 opt = qml.AdamOptimizer(stepsize=0.01) 

727 

728 def cost(params): 

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

730 

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

732 

733 

734@pytest.mark.unittest 

735def test_pauli_circuit_model() -> None: 

736 test_cases = [ 

737 { 

738 "shots": None, 

739 "output_qubit": 0, 

740 "force_mean": False, 

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

742 }, 

743 { 

744 "shots": None, 

745 "output_qubit": -1, 

746 "force_mean": False, 

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

748 }, 

749 { 

750 "shots": None, 

751 "output_qubit": -1, 

752 "force_mean": True, 

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

754 }, 

755 { 

756 "shots": 1024, 

757 "output_qubit": 0, 

758 "force_mean": False, 

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

760 }, 

761 { 

762 "shots": 1024, 

763 "output_qubit": 0, 

764 "force_mean": True, 

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

766 }, 

767 { 

768 "shots": None, 

769 "output_qubit": 0, 

770 "force_mean": False, 

771 "inputs": None, 

772 }, 

773 { 

774 "shots": None, 

775 "output_qubit": -1, 

776 "force_mean": False, 

777 "inputs": None, 

778 }, 

779 { 

780 "shots": None, 

781 "output_qubit": -1, 

782 "force_mean": True, 

783 "inputs": None, 

784 }, 

785 { 

786 "shots": 1024, 

787 "output_qubit": 0, 

788 "force_mean": False, 

789 "inputs": None, 

790 }, 

791 { 

792 "shots": 1024, 

793 "output_qubit": 0, 

794 "force_mean": True, 

795 "inputs": None, 

796 }, 

797 ] 

798 

799 for test_case in test_cases: 

800 model = Model( 

801 n_qubits=3, 

802 n_layers=2, 

803 circuit_type="Circuit_19", 

804 output_qubit=test_case["output_qubit"], 

805 shots=test_case["shots"], 

806 as_pauli_circuit=True, 

807 ) 

808 

809 pauli_model = Model( 

810 n_qubits=3, 

811 n_layers=2, 

812 circuit_type="Circuit_19", 

813 output_qubit=test_case["output_qubit"], 

814 shots=test_case["shots"], 

815 as_pauli_circuit=True, 

816 ) 

817 

818 result_circuit = model( 

819 model.params, 

820 inputs=test_case["inputs"], 

821 force_mean=test_case["force_mean"], 

822 ) 

823 

824 result_pauli_circuit = pauli_model( 

825 pauli_model.params, 

826 inputs=test_case["inputs"], 

827 force_mean=test_case["force_mean"], 

828 ) 

829 

830 assert all( 

831 np.isclose(result_circuit, result_pauli_circuit, atol=1e-5).flatten() 

832 ), ( 

833 f"results of Pauli Circuit and basic Ansatz should be equal, but " 

834 f"are {result_pauli_circuit} and {result_circuit} for testcase " 

835 f"{test_case}, respectively." 

836 )