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
« 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
13from typing import List, Callable
15logger = logging.getLogger(__name__)
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 ]
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()
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 )
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 )
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)
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 ]
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 )
169 assert isinstance(model._enc, test_case["type"])
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)
180 model = Model(
181 n_qubits=2,
182 n_layers=1,
183 circuit_type="Circuit_19",
184 )
186 result = model(
187 model.params,
188 inputs=None,
189 cache=True,
190 )
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()
208 cache_folder: str = ".cache"
209 if not os.path.exists(cache_folder):
210 raise Exception("Cache folder does not exist.")
212 name: str = f"pqc_{hs}.npy"
213 file_path: str = os.path.join(cache_folder, name)
215 if os.path.isfile(file_path):
216 cached_result = np.load(file_path)
218 assert np.array_equal(
219 result, cached_result
220 ), "Cached result and calcualted result is not equal."
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"
233 _ = model(
234 model.params,
235 inputs=None,
236 )
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 )
247 repr(model)
248 _ = model.draw(figure=True)
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 ]
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 )
282 _ = model(
283 model.params,
284 inputs=None,
285 noise_params=None,
286 cache=False,
287 execution_type="expval",
288 )
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 )
301 assert model.params.max() <= 0, "Parameters should be in [-2pi, 0]!"
303 temp_params = model.params.copy()
305 model.initialize_params(rng=np.random.default_rng(seed=1001))
307 assert not np.allclose(
308 model.params, temp_params, atol=1e-3
309 ), "Re-Initialization failed!"
312@pytest.mark.smoketest
313def test_ansaetze() -> None:
314 ansatz_cases = Ansaetze.get_available()
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 )
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 )
346 class custom_ansatz(Circuit):
347 @staticmethod
348 def n_params_per_layer(n_qubits: int) -> int:
349 return n_qubits * 3
351 @staticmethod
352 def get_control_indices(n_qubits: int) -> Optional[np.ndarray]:
353 return None
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
364 if n_qubits > 1:
365 for q in range(n_qubits - 1):
366 Gates.CZ(wires=[q, q + 1], noise_params=noise_params)
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)}")
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 )
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 )
404@pytest.mark.unittest
405def test_available_ansaetze() -> None:
406 ansatze = set(Ansaetze.get_available())
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
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)
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 )
447 out = model(
448 model.params,
449 inputs=inputs,
450 noise_params=None,
451 cache=False,
452 execution_type="expval",
453 )
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"
469@pytest.mark.smoketest
470def test_dru() -> None:
471 dru_cases = [False, True]
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 )
484 _ = model(
485 model.params,
486 inputs=None,
487 noise_params=None,
488 cache=False,
489 execution_type="expval",
490 )
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 ]
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 )
525 # Check default values
526 assert model.noise_params is None
527 assert model.execution_type == "expval"
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 )
539 model.noise_params = test_case["noise_params"]
540 model.execution_type = test_case["execution_type"]
542 _ = model(
543 model.params,
544 inputs=None,
545 noise_params=None,
546 cache=False,
547 )
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"]
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 )
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 )
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"]
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 ]
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 )
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}"
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 )
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!
714 assert not np.allclose(
715 result_a, result_b
716 ), f"Models should be different! Got {result_a} and {result_b}"
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)
728 def cost(params):
729 return model(params=params, inputs=np.array([0])).mean()._value
731 params, cost = opt.step_and_cost(cost, model.params)
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 ]
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 )
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 )
818 result_circuit = model(
819 model.params,
820 inputs=test_case["inputs"],
821 force_mean=test_case["force_mean"],
822 )
824 result_pauli_circuit = pauli_model(
825 pauli_model.params,
826 inputs=test_case["inputs"],
827 force_mean=test_case["force_mean"],
828 )
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 )