Coverage for tests/test_model.py: 98%

177 statements  

« prev     ^ index     » next       coverage.py v7.6.10, created at 2025-01-23 11:23 +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": 0, 

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 # check for 2 because of n qubits 

133 assert ( 

134 result.shape[0] == 2 

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

136 str(model) 

137 

138 

139@pytest.mark.smoketest 

140def test_encoding() -> None: 

141 test_cases = [ 

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

143 {"encoding_unitary": [Gates.RX, Gates.RY], "type": List, "input": [[0, 0]]}, 

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

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

146 ] 

147 

148 for test_case in test_cases: 

149 model = Model( 

150 n_qubits=2, 

151 n_layers=1, 

152 circuit_type="Circuit_19", 

153 encoding=test_case["encoding_unitary"], 

154 ) 

155 _ = model( 

156 model.params, 

157 inputs=test_case["input"], 

158 ) 

159 

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

161 

162 

163@pytest.mark.unittest 

164def test_cache() -> None: 

165 # Stupid try removing caches 

166 try: 

167 shutil.rmtree(".cache") 

168 except Exception as e: 

169 logger.warning(e) 

170 

171 model = Model( 

172 n_qubits=2, 

173 n_layers=1, 

174 circuit_type="Circuit_19", 

175 ) 

176 

177 result = model( 

178 model.params, 

179 inputs=None, 

180 cache=True, 

181 ) 

182 

183 hs = hashlib.md5( 

184 repr( 

185 { 

186 "n_qubits": model.n_qubits, 

187 "n_layers": model.n_layers, 

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

189 "dru": model.data_reupload, 

190 "params": model.params, 

191 "noise_params": model.noise_params, 

192 "execution_type": model.execution_type, 

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

194 "output_qubit": model.output_qubit, 

195 } 

196 ).encode("utf-8") 

197 ).hexdigest() 

198 

199 cache_folder: str = ".cache" 

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

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

202 

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

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

205 

206 if os.path.isfile(file_path): 

207 cached_result = np.load(file_path) 

208 

209 assert np.array_equal( 

210 result, cached_result 

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

212 

213 

214@pytest.mark.expensive 

215@pytest.mark.smoketest 

216def test_lightning() -> None: 

217 model = Model( 

218 n_qubits=12, # model.lightning_threshold 

219 n_layers=1, 

220 circuit_type="Hardware_Efficient", 

221 ) 

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

223 

224 _ = model( 

225 model.params, 

226 inputs=None, 

227 ) 

228 

229 

230@pytest.mark.smoketest 

231def test_draw() -> None: 

232 model = Model( 

233 n_qubits=2, 

234 n_layers=1, 

235 circuit_type="Hardware_Efficient", 

236 ) 

237 

238 repr(model) 

239 _ = model.draw(figure=True) 

240 

241 

242@pytest.mark.smoketest 

243def test_initialization() -> None: 

244 test_cases = [ 

245 { 

246 "initialization": "random", 

247 }, 

248 { 

249 "initialization": "zeros", 

250 }, 

251 { 

252 "initialization": "zero-controlled", 

253 }, 

254 { 

255 "initialization": "pi-controlled", 

256 }, 

257 { 

258 "initialization": "pi", 

259 }, 

260 ] 

261 

262 for test_case in test_cases: 

263 model = Model( 

264 n_qubits=2, 

265 n_layers=1, 

266 circuit_type="Circuit_19", 

267 data_reupload=True, 

268 initialization=test_case["initialization"], 

269 output_qubit=0, 

270 shots=1024, 

271 ) 

272 

273 _ = model( 

274 model.params, 

275 inputs=None, 

276 noise_params=None, 

277 cache=False, 

278 execution_type="expval", 

279 ) 

280 

281 

282@pytest.mark.unittest 

283def test_re_initialization() -> None: 

284 model = Model( 

285 n_qubits=2, 

286 n_layers=1, 

287 circuit_type="Circuit_19", 

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

289 random_seed=1000, 

290 ) 

291 

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

293 

294 temp_params = model.params.copy() 

295 

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

297 

298 assert not np.allclose( 

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

300 ), "Re-Initialization failed!" 

301 

302 

303@pytest.mark.smoketest 

304def test_ansaetze() -> None: 

305 ansatz_cases = Ansaetze.get_available() 

306 

307 for ansatz in ansatz_cases: 

308 # Skipping Circuit_15, as it is not yet correctly implemented (yet) 

309 if ansatz.__name__ == "Circuit_15": 

310 continue 

311 

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

313 model = Model( 

314 n_qubits=4, 

315 n_layers=1, 

316 circuit_type=ansatz.__name__, 

317 data_reupload=False, 

318 initialization="random", 

319 output_qubit=0, 

320 shots=1024, 

321 ) 

322 

323 _ = model( 

324 model.params, 

325 inputs=None, 

326 noise_params={ 

327 "BitFlip": 0.1, 

328 "PhaseFlip": 0.2, 

329 "AmplitudeDamping": 0.3, 

330 "PhaseDamping": 0.4, 

331 "DepolarizingChannel": 0.5, 

332 }, 

333 cache=False, 

334 execution_type="density", 

335 ) 

336 

337 class custom_ansatz(Circuit): 

338 @staticmethod 

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

340 return n_qubits * 3 

341 

342 @staticmethod 

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

344 return None 

345 

346 @staticmethod 

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

348 w_idx = 0 

349 for q in range(n_qubits): 

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

351 w_idx += 1 

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

353 w_idx += 1 

354 

355 if n_qubits > 1: 

356 for q in range(n_qubits - 1): 

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

358 

359 model = Model( 

360 n_qubits=2, 

361 n_layers=1, 

362 circuit_type=custom_ansatz, 

363 data_reupload=True, 

364 initialization="random", 

365 output_qubit=0, 

366 shots=1024, 

367 ) 

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

369 

370 _ = model( 

371 model.params, 

372 inputs=None, 

373 noise_params={ 

374 "BitFlip": 0.1, 

375 "PhaseFlip": 0.2, 

376 "AmplitudeDamping": 0.3, 

377 "PhaseDamping": 0.4, 

378 "DepolarizingChannel": 0.5, 

379 }, 

380 cache=False, 

381 execution_type="density", 

382 ) 

383 

384 

385@pytest.mark.unittest 

386def test_available_ansaetze() -> None: 

387 ansatze = set(Ansaetze.get_available()) 

388 

389 actual_ansaetze = set( 

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

391 ) 

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

393 assert actual_ansaetze == ansatze 

394 

395 

396@pytest.mark.unittest 

397def test_multi_input() -> None: 

398 input_cases = [ 

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

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

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

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

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

404 ] 

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

406 input_cases.append(None) 

407 

408 for inputs in input_cases: 

409 logger.info( 

410 f"Testing input with shape: " 

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

412 ) 

413 encoding = ( 

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

415 ) 

416 model = Model( 

417 n_qubits=2, 

418 n_layers=1, 

419 circuit_type="Circuit_19", 

420 data_reupload=True, 

421 initialization="random", 

422 encoding=encoding, 

423 output_qubit=0, 

424 shots=1024, 

425 ) 

426 

427 out = model( 

428 model.params, 

429 inputs=inputs, 

430 noise_params=None, 

431 cache=False, 

432 execution_type="expval", 

433 ) 

434 

435 if inputs is not None: 

436 if len(out.shape) > 0: 

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

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

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

440 ) 

441 else: 

442 assert ( 

443 inputs.shape[0] == 1 

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

445 else: 

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

447 

448 

449@pytest.mark.smoketest 

450def test_dru() -> None: 

451 dru_cases = [False, True] 

452 

453 for dru in dru_cases: 

454 model = Model( 

455 n_qubits=2, 

456 n_layers=1, 

457 circuit_type="Circuit_19", 

458 data_reupload=dru, 

459 initialization="random", 

460 output_qubit=0, 

461 shots=1024, 

462 ) 

463 

464 _ = model( 

465 model.params, 

466 inputs=None, 

467 noise_params=None, 

468 cache=False, 

469 execution_type="expval", 

470 ) 

471 

472 

473@pytest.mark.unittest 

474def test_local_state() -> None: 

475 test_cases = [ 

476 { 

477 "noise_params": None, 

478 "execution_type": "density", 

479 }, 

480 { 

481 "noise_params": { 

482 "BitFlip": 0.1, 

483 "PhaseFlip": 0.2, 

484 "AmplitudeDamping": 0.3, 

485 "PhaseDamping": 0.4, 

486 "DepolarizingChannel": 0.5, 

487 }, 

488 "execution_type": "density", 

489 }, 

490 { 

491 "noise_params": None, 

492 "execution_type": "expval", 

493 }, 

494 ] 

495 

496 model = Model( 

497 n_qubits=2, 

498 n_layers=1, 

499 circuit_type="Circuit_19", 

500 data_reupload=True, 

501 initialization="random", 

502 output_qubit=0, 

503 ) 

504 

505 # Check default values 

506 assert model.noise_params is None 

507 assert model.execution_type == "expval" 

508 

509 for test_case in test_cases: 

510 model = Model( 

511 n_qubits=2, 

512 n_layers=1, 

513 circuit_type="Circuit_19", 

514 data_reupload=True, 

515 initialization="random", 

516 output_qubit=0, 

517 ) 

518 

519 model.noise_params = test_case["noise_params"] 

520 model.execution_type = test_case["execution_type"] 

521 

522 _ = model( 

523 model.params, 

524 inputs=None, 

525 noise_params=None, 

526 cache=False, 

527 ) 

528 

529 # check if setting "externally" is working 

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

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

532 

533 model = Model( 

534 n_qubits=2, 

535 n_layers=1, 

536 circuit_type="Circuit_19", 

537 data_reupload=True, 

538 initialization="random", 

539 output_qubit=0, 

540 ) 

541 

542 _ = model( 

543 model.params, 

544 inputs=None, 

545 cache=False, 

546 noise_params=test_case["noise_params"], 

547 execution_type=test_case["execution_type"], 

548 ) 

549 

550 # check if setting in the forward call is working 

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

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

553 

554 

555@pytest.mark.unittest 

556def test_local_and_global_meas() -> None: 

557 test_cases = [ 

558 { 

559 "inputs": None, 

560 "execution_type": "expval", 

561 "output_qubit": -1, 

562 "shots": None, 

563 "out_shape": (2,), 

564 "warning": False, 

565 }, 

566 { 

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

568 "execution_type": "expval", 

569 "output_qubit": -1, 

570 "shots": None, 

571 "out_shape": (2, 3), 

572 "warning": False, 

573 }, 

574 { 

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

576 "execution_type": "expval", 

577 "output_qubit": 0, 

578 "shots": None, 

579 "out_shape": (3,), 

580 "warning": False, 

581 }, 

582 { 

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

584 "execution_type": "expval", 

585 "output_qubit": [0, 1], 

586 "shots": None, 

587 "out_shape": (3,), 

588 "warning": False, 

589 }, 

590 { 

591 "inputs": None, 

592 "execution_type": "density", 

593 "output_qubit": -1, 

594 "shots": None, 

595 "out_shape": (4, 4), 

596 "warning": False, 

597 }, 

598 { 

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

600 "execution_type": "density", 

601 "output_qubit": -1, 

602 "shots": None, 

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

604 "warning": False, 

605 }, 

606 { 

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

608 "execution_type": "density", 

609 "output_qubit": 0, 

610 "shots": None, 

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

612 "warning": True, 

613 }, 

614 { 

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

616 "execution_type": "probs", 

617 "output_qubit": -1, 

618 "shots": 1024, 

619 "out_shape": (3, 4), 

620 "warning": False, 

621 }, 

622 { 

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

624 "execution_type": "probs", 

625 "output_qubit": 0, 

626 "shots": 1024, 

627 "out_shape": (3, 2), 

628 "warning": False, 

629 }, 

630 { 

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

632 "execution_type": "probs", 

633 "output_qubit": [0, 1], 

634 "shots": 1024, 

635 "out_shape": (3, 4), 

636 "warning": False, 

637 }, 

638 ] 

639 

640 for test_case in test_cases: 

641 model = Model( 

642 n_qubits=2, 

643 n_layers=1, 

644 circuit_type="Circuit_19", 

645 data_reupload=True, 

646 initialization="random", 

647 output_qubit=test_case["output_qubit"], 

648 shots=test_case["shots"], 

649 ) 

650 if test_case["warning"]: 

651 with pytest.warns(UserWarning): 

652 out = model( 

653 model.params, 

654 inputs=test_case["inputs"], 

655 noise_params=None, 

656 cache=False, 

657 execution_type=test_case["execution_type"], 

658 ) 

659 else: 

660 out = model( 

661 model.params, 

662 inputs=test_case["inputs"], 

663 noise_params=None, 

664 cache=False, 

665 execution_type=test_case["execution_type"], 

666 ) 

667 

668 assert ( 

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

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

671 for test case {test_case}" 

672 

673 

674@pytest.mark.unittest 

675def test_parity() -> None: 

676 model_a = Model( 

677 n_qubits=2, 

678 n_layers=1, 

679 circuit_type="Circuit_1", 

680 output_qubit=[0, 1], # parity 

681 ) 

682 model_b = Model( 

683 n_qubits=2, 

684 n_layers=1, 

685 circuit_type="Circuit_1", 

686 output_qubit=-1, # individual 

687 ) 

688 

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

690 result_b = model_b( 

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

692 ) # use same params! 

693 

694 assert not np.allclose( 

695 result_a, result_b 

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

697 

698 

699@pytest.mark.smoketest 

700def test_params_store() -> None: 

701 model = Model( 

702 n_qubits=2, 

703 n_layers=1, 

704 circuit_type="Circuit_1", 

705 ) 

706 opt = qml.AdamOptimizer(stepsize=0.01) 

707 

708 def cost(params): 

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

710 

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