2121
2222import numpy as np
2323import pytest
24- import torch
24+
25+ torch = pytest .importorskip ("torch" )
2526
2627from .qdp_test_utils import requires_qdp
2728
2829
30+ @pytest .fixture (autouse = True )
31+ def require_cuda ():
32+ if not torch .cuda .is_available ():
33+ pytest .skip ("GPU required for QdpEngine" )
34+
35+
2936def _verify_tensor (tensor , expected_shape , check_normalization = False ):
3037 """Helper function to verify tensor properties"""
3138 assert tensor .shape == expected_shape , (
@@ -54,10 +61,6 @@ def test_encode_from_numpy_file(num_samples, num_qubits, check_norm):
5461 """Test NumPy file encoding"""
5562 from _qdp import QdpEngine
5663
57- pytest .importorskip ("torch" )
58- if not torch .cuda .is_available ():
59- pytest .skip ("GPU required for QdpEngine" )
60-
6164 engine = QdpEngine (device_id = 0 )
6265 sample_size = 2 ** num_qubits
6366
@@ -89,10 +92,6 @@ def test_encode_numpy_array_1d(num_qubits):
8992 """Test 1D NumPy array encoding (single sample)"""
9093 from _qdp import QdpEngine
9194
92- pytest .importorskip ("torch" )
93- if not torch .cuda .is_available ():
94- pytest .skip ("GPU required for QdpEngine" )
95-
9695 engine = QdpEngine (device_id = 0 )
9796 sample_size = 2 ** num_qubits
9897 data = np .random .randn (sample_size ).astype (np .float64 )
@@ -110,10 +109,6 @@ def test_encode_numpy_array_2d(num_samples, num_qubits):
110109 """Test 2D NumPy array encoding (batch)"""
111110 from _qdp import QdpEngine
112111
113- pytest .importorskip ("torch" )
114- if not torch .cuda .is_available ():
115- pytest .skip ("GPU required for QdpEngine" )
116-
117112 engine = QdpEngine (device_id = 0 )
118113 sample_size = 2 ** num_qubits
119114 data = np .random .randn (num_samples , sample_size ).astype (np .float64 )
@@ -129,14 +124,9 @@ def test_encode_numpy_array_2d(num_samples, num_qubits):
129124@pytest .mark .gpu
130125@pytest .mark .parametrize ("encoding_method" , ["amplitude" ])
131126def test_encode_numpy_encoding_methods (encoding_method ):
132- """Test different encoding methods """
127+ """Test amplitude encoding via encoding_method parameter """
133128 from _qdp import QdpEngine
134129
135- pytest .importorskip ("torch" )
136- if not torch .cuda .is_available ():
137- pytest .skip ("GPU required for QdpEngine" )
138-
139- # TODO: Add angle and basis encoding tests when implemented
140130 engine = QdpEngine (device_id = 0 )
141131 num_qubits = 2
142132 sample_size = 2 ** num_qubits
@@ -147,6 +137,243 @@ def test_encode_numpy_encoding_methods(encoding_method):
147137 _verify_tensor (tensor , (1 , sample_size ))
148138
149139
140+ # ---------------------------------------------------------------------------
141+ # Angle encoding tests
142+ # ---------------------------------------------------------------------------
143+
144+
145+ @requires_qdp
146+ @pytest .mark .gpu
147+ @pytest .mark .parametrize ("num_qubits" , [1 , 2 , 3 , 4 ])
148+ def test_encode_numpy_angle_encoding_1d (num_qubits ):
149+ """Angle encoding: 1D array with num_qubits angles (single sample)"""
150+ from _qdp import QdpEngine
151+
152+ engine = QdpEngine (device_id = 0 )
153+ # Angle encoding expects exactly num_qubits values: one angle per qubit
154+ angles = np .random .uniform (0 , 2 * np .pi , size = num_qubits ).astype (np .float64 )
155+
156+ qtensor = engine .encode (angles , num_qubits , encoding_method = "angle" )
157+ tensor = torch .from_dlpack (qtensor )
158+
159+ _verify_tensor (tensor , (1 , 2 ** num_qubits ), check_normalization = True )
160+
161+
162+ @requires_qdp
163+ @pytest .mark .gpu
164+ @pytest .mark .parametrize (("num_samples" , "num_qubits" ), [(5 , 2 ), (10 , 3 ), (1 , 4 )])
165+ def test_encode_numpy_angle_encoding_2d (num_samples , num_qubits ):
166+ """Angle encoding: 2D array of shape (num_samples, num_qubits)"""
167+ from _qdp import QdpEngine
168+
169+ engine = QdpEngine (device_id = 0 )
170+ angles = np .random .uniform (0 , 2 * np .pi , size = (num_samples , num_qubits )).astype (
171+ np .float64
172+ )
173+
174+ qtensor = engine .encode (angles , num_qubits , encoding_method = "angle" )
175+ tensor = torch .from_dlpack (qtensor )
176+
177+ _verify_tensor (tensor , (num_samples , 2 ** num_qubits ), check_normalization = True )
178+
179+
180+ @requires_qdp
181+ @pytest .mark .gpu
182+ def test_encode_numpy_angle_encoding_from_file ():
183+ """Angle encoding: load angles from .npy file"""
184+ from _qdp import QdpEngine
185+
186+ engine = QdpEngine (device_id = 0 )
187+ num_qubits = 3
188+ num_samples = 8
189+ angles = np .random .uniform (0 , 2 * np .pi , size = (num_samples , num_qubits )).astype (
190+ np .float64
191+ )
192+
193+ with tempfile .NamedTemporaryFile (suffix = ".npy" , delete = False ) as f :
194+ npy_path = f .name
195+ try :
196+ np .save (npy_path , angles )
197+ qtensor = engine .encode (npy_path , num_qubits , encoding_method = "angle" )
198+ tensor = torch .from_dlpack (qtensor )
199+ _verify_tensor (tensor , (num_samples , 2 ** num_qubits ), check_normalization = True )
200+ finally :
201+ if os .path .exists (npy_path ):
202+ os .remove (npy_path )
203+
204+
205+ @requires_qdp
206+ @pytest .mark .gpu
207+ def test_encode_numpy_angle_encoding_wrong_sample_size ():
208+ """Angle encoding: wrong sample_size raises an error"""
209+ from _qdp import QdpEngine
210+
211+ engine = QdpEngine (device_id = 0 )
212+ num_qubits = 3
213+ # Pass 2^num_qubits values instead of num_qubits values
214+ wrong_data = np .ones (2 ** num_qubits , dtype = np .float64 )
215+
216+ with pytest .raises ((RuntimeError , ValueError )):
217+ engine .encode (wrong_data , num_qubits , encoding_method = "angle" )
218+
219+
220+ @requires_qdp
221+ @pytest .mark .gpu
222+ @pytest .mark .parametrize (
223+ ("theta" , "expected_probs" ),
224+ [
225+ pytest .param (0.0 , [1.0 , 0.0 ], id = "theta=0" ),
226+ pytest .param (np .pi / 2 , [0.0 , 1.0 ], id = "theta=pi/2" ),
227+ pytest .param (np .pi , [1.0 , 0.0 ], id = "theta=pi" ),
228+ ],
229+ )
230+ def test_encode_numpy_angle_encoding_1qubit_correctness (theta , expected_probs ):
231+ """Angle encoding: 1 qubit with known angle θ → probabilities [cos²(θ), sin²(θ)]"""
232+ from _qdp import QdpEngine
233+
234+ engine = QdpEngine (device_id = 0 , precision = "float64" )
235+ data = np .array ([theta ], dtype = np .float64 )
236+
237+ qtensor = engine .encode (data , 1 , encoding_method = "angle" )
238+ tensor = torch .from_dlpack (qtensor )
239+
240+ probs = tensor .abs ().pow (2 ).squeeze (0 ) # shape: (2,)
241+ expected = torch .tensor (expected_probs , dtype = probs .dtype , device = probs .device )
242+
243+ assert torch .allclose (probs , expected , atol = 1e-6 ), (
244+ f"For θ={ theta } , expected { expected .cpu ().tolist ()} , got { probs .cpu ().tolist ()} "
245+ )
246+
247+
248+ # ---------------------------------------------------------------------------
249+ # Basis encoding tests
250+ # ---------------------------------------------------------------------------
251+
252+
253+ @requires_qdp
254+ @pytest .mark .gpu
255+ @pytest .mark .parametrize ("num_qubits" , [1 , 2 , 3 , 4 ])
256+ def test_encode_numpy_basis_encoding_1d (num_qubits ):
257+ """Basis encoding: 1D array with a single index (single sample)"""
258+ from _qdp import QdpEngine
259+
260+ engine = QdpEngine (device_id = 0 )
261+ # Basis encoding expects sample_size=1: one integer index per sample
262+ index = np .array ([float (2 ** num_qubits - 1 )], dtype = np .float64 )
263+
264+ qtensor = engine .encode (index , num_qubits , encoding_method = "basis" )
265+ tensor = torch .from_dlpack (qtensor )
266+
267+ _verify_tensor (tensor , (1 , 2 ** num_qubits ), check_normalization = True )
268+
269+
270+ @requires_qdp
271+ @pytest .mark .gpu
272+ @pytest .mark .parametrize (("num_samples" , "num_qubits" ), [(5 , 2 ), (10 , 3 ), (1 , 4 )])
273+ def test_encode_numpy_basis_encoding_2d (num_samples , num_qubits ):
274+ """Basis encoding: 2D array of shape (num_samples, 1) with integer indices"""
275+ from _qdp import QdpEngine
276+
277+ engine = QdpEngine (device_id = 0 )
278+ max_index = 2 ** num_qubits
279+ indices = np .random .randint (0 , max_index , size = (num_samples , 1 )).astype (np .float64 )
280+
281+ qtensor = engine .encode (indices , num_qubits , encoding_method = "basis" )
282+ tensor = torch .from_dlpack (qtensor )
283+
284+ _verify_tensor (tensor , (num_samples , 2 ** num_qubits ), check_normalization = True )
285+
286+
287+ @requires_qdp
288+ @pytest .mark .gpu
289+ def test_encode_numpy_basis_encoding_from_file ():
290+ """Basis encoding: load indices from .npy file"""
291+ from _qdp import QdpEngine
292+
293+ engine = QdpEngine (device_id = 0 )
294+ num_qubits = 3
295+ num_samples = 6
296+ indices = np .random .randint (0 , 2 ** num_qubits , size = (num_samples , 1 )).astype (
297+ np .float64
298+ )
299+
300+ with tempfile .NamedTemporaryFile (suffix = ".npy" , delete = False ) as f :
301+ npy_path = f .name
302+ try :
303+ np .save (npy_path , indices )
304+ qtensor = engine .encode (npy_path , num_qubits , encoding_method = "basis" )
305+ tensor = torch .from_dlpack (qtensor )
306+ _verify_tensor (tensor , (num_samples , 2 ** num_qubits ), check_normalization = True )
307+ finally :
308+ if os .path .exists (npy_path ):
309+ os .remove (npy_path )
310+
311+
312+ @requires_qdp
313+ @pytest .mark .gpu
314+ @pytest .mark .parametrize (
315+ ("basis_index" , "num_qubits" ),
316+ [
317+ (0 , 2 ), # |00⟩: amplitude 1 at index 0
318+ (1 , 2 ), # |01⟩: amplitude 1 at index 1
319+ (3 , 2 ), # |11⟩: amplitude 1 at index 3
320+ (0 , 3 ), # |000⟩: amplitude 1 at index 0
321+ (13 , 4 ), # |1101⟩: amplitude 1 at index 13
322+ ],
323+ )
324+ def test_encode_numpy_basis_state_correctness (basis_index , num_qubits ):
325+ """Basis encoding: verify the encoded state is exactly |basis_index⟩"""
326+ from _qdp import QdpEngine
327+
328+ engine = QdpEngine (device_id = 0 , precision = "float64" )
329+ data = np .array ([[float (basis_index )]], dtype = np .float64 )
330+
331+ qtensor = engine .encode (data , num_qubits , encoding_method = "basis" )
332+ tensor = torch .from_dlpack (qtensor )
333+
334+ # The encoded state should be a one-hot complex vector with |amplitude|²=1
335+ # at basis_index and 0 elsewhere
336+ probs = tensor .abs ().pow (2 ).squeeze (0 ) # shape: (2^num_qubits,)
337+ expected = torch .zeros (2 ** num_qubits , dtype = probs .dtype , device = probs .device )
338+ expected [basis_index ] = 1.0
339+
340+ assert torch .allclose (probs , expected , atol = 1e-6 ), (
341+ f"Expected |{ basis_index } ⟩ but got probabilities { probs .cpu ().tolist ()} "
342+ )
343+
344+
345+ @requires_qdp
346+ @pytest .mark .gpu
347+ @pytest .mark .parametrize (
348+ "bad_data" ,
349+ [
350+ pytest .param (np .array ([[- 1.0 ]], dtype = np .float64 ), id = "negative index" ),
351+ pytest .param (np .array ([[0.5 ]], dtype = np .float64 ), id = "non-integer index" ),
352+ ],
353+ )
354+ def test_encode_numpy_basis_encoding_invalid_index (bad_data ):
355+ """Basis encoding: invalid index values raise an error"""
356+ from _qdp import QdpEngine
357+
358+ engine = QdpEngine (device_id = 0 )
359+ with pytest .raises ((RuntimeError , ValueError )):
360+ engine .encode (bad_data , 2 , encoding_method = "basis" )
361+
362+
363+ @requires_qdp
364+ @pytest .mark .gpu
365+ def test_encode_numpy_basis_encoding_out_of_range ():
366+ """Basis encoding: index >= 2^num_qubits raises an error"""
367+ from _qdp import QdpEngine
368+
369+ engine = QdpEngine (device_id = 0 )
370+ num_qubits = 2
371+ out_of_range = np .array ([[float (2 ** num_qubits )]], dtype = np .float64 ) # index == 4
372+
373+ with pytest .raises ((RuntimeError , ValueError )):
374+ engine .encode (out_of_range , num_qubits , encoding_method = "basis" )
375+
376+
150377@requires_qdp
151378@pytest .mark .gpu
152379@pytest .mark .parametrize (
@@ -160,10 +387,6 @@ def test_encode_numpy_precision(precision, expected_dtype):
160387 """Test different precision settings"""
161388 from _qdp import QdpEngine
162389
163- pytest .importorskip ("torch" )
164- if not torch .cuda .is_available ():
165- pytest .skip ("GPU required for QdpEngine" )
166-
167390 engine = QdpEngine (device_id = 0 , precision = precision )
168391 num_qubits = 2
169392 data = np .array ([1.0 , 2.0 , 3.0 , 4.0 ], dtype = np .float64 )
@@ -194,10 +417,6 @@ def test_encode_numpy_errors(data, error_match):
194417 """Test error handling for invalid inputs"""
195418 from _qdp import QdpEngine
196419
197- pytest .importorskip ("torch" )
198- if not torch .cuda .is_available ():
199- pytest .skip ("GPU required for QdpEngine" )
200-
201420 engine = QdpEngine (device_id = 0 )
202421 num_qubits = 2 if data .ndim == 1 else 1
203422
0 commit comments