2828
2929The test structure and helper usage mirror dpnp/tests/test_linalg.py so that
3030the suite fits naturally into the existing CI infrastructure.
31+
32+ Note: dpnp.ndarray deliberately blocks implicit numpy conversion (raises
33+ TypeError in __array__) to prevent silent dtype=object arrays. All
34+ assertions that need a host-side NumPy array must call `arr.asnumpy()`
35+ explicitly instead of `numpy.asarray(arr)`.
3136"""
3237
3338import numpy
5459# Helpers
5560# ---------------------------------------------------------------------------
5661
62+ def _to_numpy (x ):
63+ """Convert a dpnp array (or plain numpy array) to numpy safely."""
64+ if isinstance (x , dpnp .ndarray ):
65+ return x .asnumpy ()
66+ return numpy .asarray (x )
67+
68+
5769def _make_spd (n , dtype , rng ):
5870 """Return a symmetric positive-definite matrix of size n."""
5971 A = rng .standard_normal ((n , n )).astype (dtype )
@@ -76,7 +88,7 @@ def _make_nonsym(n, dtype, rng):
7688
7789def _rel_residual (A_np , x_dp , b_np ):
7890 """Relative residual ||Ax - b|| / ||b||."""
79- x_np = numpy . asarray (x_dp )
91+ x_np = _to_numpy (x_dp )
8092 r = A_np @ x_np - b_np
8193 b_nrm = numpy .linalg .norm (b_np )
8294 return numpy .linalg .norm (r ) / (b_nrm if b_nrm > 0 else 1.0 )
@@ -118,7 +130,7 @@ def test_matvec_identity(self, n):
118130 op = LinearOperator ((n , n ), matvec = lambda x : A_dp @ x )
119131 x_dp = dpnp .arange (n , dtype = numpy .float64 )
120132 y_dp = op .matvec (x_dp )
121- assert_allclose (numpy . asarray (y_dp ), numpy . asarray (x_dp ), rtol = 1e-12 )
133+ assert_allclose (_to_numpy (y_dp ), _to_numpy (x_dp ), rtol = 1e-12 )
122134
123135 @pytest .mark .parametrize ("dtype" , [numpy .float32 , numpy .float64 ])
124136 def test_matvec_dense (self , dtype ):
@@ -132,7 +144,7 @@ def test_matvec_dense(self, dtype):
132144 op = LinearOperator ((n , n ), matvec = lambda x : A_dp @ x , dtype = dtype )
133145 y_dp = op .matvec (x_dp )
134146 y_ref = A_np @ x_np
135- assert_allclose (numpy . asarray (y_dp ), y_ref , rtol = 1e-5 )
147+ assert_allclose (_to_numpy (y_dp ), y_ref , rtol = 1e-5 )
136148
137149 # --- rmatvec ---
138150
@@ -151,7 +163,7 @@ def test_rmatvec_defined(self):
151163 )
152164 y_dp = op .rmatvec (x_dp )
153165 y_ref = A_np .T @ x_np
154- assert_allclose (numpy . asarray (y_dp ), y_ref , rtol = 1e-12 )
166+ assert_allclose (_to_numpy (y_dp ), y_ref , rtol = 1e-12 )
155167
156168 def test_rmatvec_not_defined_raises (self ):
157169 n = 4
@@ -174,7 +186,7 @@ def test_matmat_fallback_loop(self):
174186 op = LinearOperator ((n , n ), matvec = lambda x : A_dp @ x )
175187 Y_dp = op .matmat (X_dp )
176188 Y_ref = A_np @ X_np
177- assert_allclose (numpy . asarray (Y_dp ), Y_ref , rtol = 1e-10 )
189+ assert_allclose (_to_numpy (Y_dp ), Y_ref , rtol = 1e-10 )
178190
179191 def test_matmat_explicit (self ):
180192 rng = numpy .random .default_rng (3 )
@@ -190,7 +202,7 @@ def test_matmat_explicit(self):
190202 matmat = lambda X : A_dp @ X ,
191203 )
192204 Y_dp = op .matmat (X_dp )
193- assert_allclose (numpy . asarray (Y_dp ), A_np @ X_np , rtol = 1e-10 )
205+ assert_allclose (_to_numpy (Y_dp ), A_np @ X_np , rtol = 1e-10 )
194206
195207 # --- __matmul__ / __call__ ---
196208
@@ -200,22 +212,22 @@ def test_matmul_1d(self):
200212 op = LinearOperator ((n , n ), matvec = lambda x : A_dp @ x )
201213 x_dp = dpnp .ones (n )
202214 y_dp = op @ x_dp
203- assert_allclose (numpy . asarray (y_dp ), numpy .full (n , 2.0 ))
215+ assert_allclose (_to_numpy (y_dp ), numpy .full (n , 2.0 ))
204216
205217 def test_matmul_2d (self ):
206218 n , k = 4 , 3
207219 A_dp = dpnp .eye (n , dtype = numpy .float64 )
208220 X_dp = dpnp .ones ((n , k ))
209221 op = LinearOperator ((n , n ), matvec = lambda x : A_dp @ x )
210222 Y_dp = op @ X_dp
211- assert_allclose (numpy . asarray (Y_dp ), numpy .ones ((n , k )))
223+ assert_allclose (_to_numpy (Y_dp ), numpy .ones ((n , k )))
212224
213225 def test_call_delegates_to_matmul (self ):
214226 n = 4
215227 A_dp = dpnp .eye (n , dtype = numpy .float64 )
216228 op = LinearOperator ((n , n ), matvec = lambda x : A_dp @ x )
217229 x_dp = dpnp .ones (n )
218- assert_allclose (numpy . asarray (op (x_dp )), numpy . asarray (op @ x_dp ))
230+ assert_allclose (_to_numpy (op (x_dp )), _to_numpy (op @ x_dp ))
219231
220232 # --- operator algebra ---
221233
@@ -231,8 +243,8 @@ def test_adjoint_property_H(self):
231243 )
232244 x_dp = dpnp .asarray (rng .standard_normal (n ))
233245 y_H = op .H .matvec (x_dp )
234- y_ref = A_np .T @ numpy . asarray (x_dp )
235- assert_allclose (numpy . asarray (y_H ), y_ref , rtol = 1e-12 )
246+ y_ref = A_np .T @ _to_numpy (x_dp )
247+ assert_allclose (_to_numpy (y_H ), y_ref , rtol = 1e-12 )
236248
237249 def test_transpose_property_T (self ):
238250 rng = numpy .random .default_rng (5 )
@@ -247,8 +259,8 @@ def test_transpose_property_T(self):
247259 x_dp = dpnp .asarray (rng .standard_normal (n ))
248260 y_T = op .T .matvec (x_dp )
249261 # For real A, T == H
250- y_ref = A_np .T @ numpy . asarray (x_dp )
251- assert_allclose (numpy . asarray (y_T ), y_ref , rtol = 1e-12 )
262+ y_ref = A_np .T @ _to_numpy (x_dp )
263+ assert_allclose (_to_numpy (y_T ), y_ref , rtol = 1e-12 )
252264
253265 def test_add_two_operators (self ):
254266 n = 5
@@ -259,7 +271,7 @@ def test_add_two_operators(self):
259271 opC = opA + opB
260272 x_dp = dpnp .ones (n )
261273 y_dp = opC .matvec (x_dp )
262- assert_allclose (numpy . asarray (y_dp ), numpy .full (n , 3.0 ))
274+ assert_allclose (_to_numpy (y_dp ), numpy .full (n , 3.0 ))
263275
264276 def test_scalar_multiply (self ):
265277 n = 4
@@ -268,7 +280,7 @@ def test_scalar_multiply(self):
268280 op3 = op * 3.0
269281 x_dp = dpnp .ones (n )
270282 y_dp = op3 .matvec (x_dp )
271- assert_allclose (numpy . asarray (y_dp ), numpy .full (n , 3.0 ))
283+ assert_allclose (_to_numpy (y_dp ), numpy .full (n , 3.0 ))
272284
273285 def test_product_operator (self ):
274286 n = 5
@@ -279,7 +291,7 @@ def test_product_operator(self):
279291 opAB = opA * opB
280292 x_dp = dpnp .ones (n )
281293 y_dp = opAB .matvec (x_dp )
282- assert_allclose (numpy . asarray (y_dp ), numpy .full (n , 6.0 ))
294+ assert_allclose (_to_numpy (y_dp ), numpy .full (n , 6.0 ))
283295
284296 def test_neg_operator (self ):
285297 n = 4
@@ -288,7 +300,7 @@ def test_neg_operator(self):
288300 neg_op = - op
289301 x_dp = dpnp .ones (n )
290302 y_dp = neg_op .matvec (x_dp )
291- assert_allclose (numpy . asarray (y_dp ), numpy .full (n , - 1.0 ))
303+ assert_allclose (_to_numpy (y_dp ), numpy .full (n , - 1.0 ))
292304
293305 def test_power_operator (self ):
294306 n = 4
@@ -298,7 +310,7 @@ def test_power_operator(self):
298310 x_dp = dpnp .ones (n )
299311 y_dp = op3 .matvec (x_dp )
300312 # 2^3 * I * [1...] = 8
301- assert_allclose (numpy . asarray (y_dp ), numpy .full (n , 8.0 ))
313+ assert_allclose (_to_numpy (y_dp ), numpy .full (n , 8.0 ))
302314
303315 # --- shape / error validation ---
304316
@@ -327,15 +339,15 @@ def test_aslinearoperator_from_dense_dpnp(self):
327339 op = aslinearoperator (A_dp )
328340 x_dp = dpnp .ones (n )
329341 y_dp = op .matvec (x_dp )
330- assert_allclose (numpy . asarray (y_dp ), numpy .ones (n ))
342+ assert_allclose (_to_numpy (y_dp ), numpy .ones (n ))
331343
332344 def test_aslinearoperator_from_numpy (self ):
333345 n = 5
334346 A_np = numpy .eye (n , dtype = numpy .float64 )
335347 op = aslinearoperator (A_np )
336348 x_dp = dpnp .ones (n )
337349 y_dp = op .matvec (x_dp )
338- assert_allclose (numpy . asarray (y_dp ), numpy .ones (n ))
350+ assert_allclose (_to_numpy (y_dp ), numpy .ones (n ))
339351
340352 def test_aslinearoperator_invalid_raises (self ):
341353 with pytest .raises (TypeError ):
@@ -355,8 +367,8 @@ def test_identity_operator(self):
355367 n = 7
356368 op = IdentityOperator ((n , n ), dtype = numpy .float64 )
357369 x_dp = dpnp .arange (n , dtype = numpy .float64 )
358- assert_array_equal (numpy . asarray (op .matvec (x_dp )), numpy .arange (n ))
359- assert_array_equal (numpy . asarray (op .rmatvec (x_dp )), numpy .arange (n ))
370+ assert_array_equal (_to_numpy (op .matvec (x_dp )), numpy .arange (n ))
371+ assert_array_equal (_to_numpy (op .rmatvec (x_dp )), numpy .arange (n ))
360372
361373 # --- complex dtype ---
362374
@@ -371,7 +383,7 @@ def test_complex_matvec(self, dtype):
371383
372384 op = LinearOperator ((n , n ), matvec = lambda x : A_dp @ x , dtype = dtype )
373385 y_dp = op .matvec (x_dp )
374- assert_allclose (numpy . asarray (y_dp ), A_np @ x_np , rtol = 1e-4 )
386+ assert_allclose (_to_numpy (y_dp ), A_np @ x_np , rtol = 1e-4 )
375387
376388
377389# ---------------------------------------------------------------------------
@@ -406,7 +418,7 @@ def test_cg_matches_numpy_solve(self):
406418 x_ref = numpy .linalg .solve (A_np , b_np )
407419 x_dp , info = cg (A_dp , b_dp , tol = 1e-10 , maxiter = 1000 )
408420 assert info == 0
409- assert_allclose (numpy . asarray (x_dp ), x_ref , rtol = 1e-6 )
421+ assert_allclose (_to_numpy (x_dp ), x_ref , rtol = 1e-6 )
410422
411423 def test_cg_x0_initial_guess (self ):
412424 rng = numpy .random .default_rng (102 )
@@ -417,11 +429,9 @@ def test_cg_x0_initial_guess(self):
417429 A_dp = dpnp .asarray (A_np )
418430 b_dp = dpnp .asarray (b_np )
419431
420- # Start from a good initial guess: actual solution
421432 x_ref = numpy .linalg .solve (A_np , b_np )
422433 x0_dp = dpnp .asarray (x_ref )
423434 x_dp , info = cg (A_dp , b_dp , x0 = x0_dp , tol = 1e-10 , maxiter = 5 )
424- # Should converge immediately or with very few iterations
425435 assert _rel_residual (A_np , x_dp , b_np ) < 1e-8
426436
427437 def test_cg_callback_called (self ):
@@ -447,7 +457,7 @@ def test_cg_already_zero_rhs(self):
447457 b_dp = dpnp .zeros (n , dtype = numpy .float64 )
448458 x_dp , info = cg (A_dp , b_dp )
449459 assert info == 0
450- assert_allclose (numpy . asarray (x_dp ), numpy .zeros (n ), atol = 1e-14 )
460+ assert_allclose (_to_numpy (x_dp ), numpy .zeros (n ), atol = 1e-14 )
451461
452462 def test_cg_returns_dpnp_array (self ):
453463 n = 4
@@ -509,7 +519,6 @@ def test_cg_dtype_preserved_in_output(self, dtype):
509519 A_np = _make_spd (n , dtype , rng )
510520 b_np = rng .standard_normal (n ).astype (dtype )
511521 x_dp , _ = cg (dpnp .asarray (A_np ), dpnp .asarray (b_np ), tol = 1e-6 , maxiter = 500 )
512- # Result should be float64 (working precision) or at least same family
513522 assert numpy .issubdtype (x_dp .dtype , numpy .floating )
514523
515524
@@ -545,7 +554,7 @@ def test_gmres_matches_numpy_solve(self):
545554 x_ref = numpy .linalg .solve (A_np , b_np )
546555 x_dp , info = gmres (A_dp , b_dp , tol = 1e-10 , maxiter = 50 , restart = n )
547556 assert info == 0
548- assert_allclose (numpy . asarray (x_dp ), x_ref , rtol = 1e-5 )
557+ assert_allclose (_to_numpy (x_dp ), x_ref , rtol = 1e-5 )
549558
550559 def test_gmres_spd_matches_cg (self ):
551560 """On an SPD system GMRES and CG should agree."""
@@ -559,7 +568,7 @@ def test_gmres_spd_matches_cg(self):
559568
560569 x_gmres , _ = gmres (A_dp , b_dp , tol = 1e-10 , maxiter = 100 , restart = n )
561570 x_cg , _ = cg (A_dp , b_dp , tol = 1e-10 , maxiter = 500 )
562- assert_allclose (numpy . asarray (x_gmres ), numpy . asarray (x_cg ), rtol = 1e-5 )
571+ assert_allclose (_to_numpy (x_gmres ), _to_numpy (x_cg ), rtol = 1e-5 )
563572
564573 def test_gmres_restart_parameter (self ):
565574 """Restarted GMRES (restart < n) should still converge."""
@@ -611,7 +620,7 @@ def test_gmres_already_zero_rhs(self):
611620 b_dp = dpnp .zeros (n , dtype = numpy .float64 )
612621 x_dp , info = gmres (A_dp , b_dp )
613622 assert info == 0
614- assert_allclose (numpy . asarray (x_dp ), numpy .zeros (n ), atol = 1e-14 )
623+ assert_allclose (_to_numpy (x_dp ), numpy .zeros (n ), atol = 1e-14 )
615624
616625 def test_gmres_returns_dpnp_array (self ):
617626 n = 4
@@ -706,7 +715,7 @@ def test_gmres_happy_breakdown(self, n):
706715 b_dp = dpnp .arange (1 , n + 1 , dtype = numpy .float64 )
707716 x_dp , info = gmres (A_dp , b_dp , tol = 1e-12 , maxiter = n , restart = n )
708717 assert info == 0
709- assert_allclose (numpy . asarray (x_dp ), numpy .arange (1 , n + 1 ), rtol = 1e-10 )
718+ assert_allclose (_to_numpy (x_dp ), numpy .arange (1 , n + 1 ), rtol = 1e-10 )
710719
711720
712721# ---------------------------------------------------------------------------
@@ -760,7 +769,7 @@ def test_minres_matches_scipy(self):
760769 dpnp .asarray (A_np ), dpnp .asarray (b_np ), tol = 1e-10
761770 )
762771 assert info_dp == 0
763- assert_allclose (numpy . asarray (x_dp ), x_scipy , rtol = 1e-6 )
772+ assert_allclose (_to_numpy (x_dp ), x_scipy , rtol = 1e-6 )
764773
765774 def test_minres_x0_initial_guess (self ):
766775 rng = numpy .random .default_rng (303 )
@@ -789,7 +798,7 @@ def test_minres_already_zero_rhs(self):
789798 b_dp = dpnp .zeros (n , dtype = numpy .float64 )
790799 x_dp , info = minres (A_dp , b_dp )
791800 assert info == 0
792- assert_allclose (numpy . asarray (x_dp ), numpy .zeros (n ), atol = 1e-14 )
801+ assert_allclose (_to_numpy (x_dp ), numpy .zeros (n ), atol = 1e-14 )
793802
794803 def test_minres_non_square_raises (self ):
795804 A_dp = dpnp .ones ((4 , 6 ), dtype = numpy .float64 )
@@ -806,7 +815,6 @@ def test_minres_with_shift(self):
806815 A_dp = dpnp .asarray (A_np )
807816 b_dp = dpnp .asarray (b_np )
808817
809- # shift = 0 should be the default behaviour
810818 x_dp , info = minres (A_dp , b_dp , tol = 1e-8 , shift = 0.0 )
811819 assert info == 0
812820 assert _rel_residual (A_np , x_dp , b_np ) < 1e-6
@@ -834,7 +842,6 @@ def test_minres_with_preconditioner(self):
834842 b_np = rng .standard_normal (n ).astype (dtype )
835843 b_dp = dpnp .asarray (b_np )
836844
837- # Use diagonal preconditioner M ≈ diag(A)^{-1}
838845 diag_A = numpy .diag (A_np )
839846 M_np = numpy .diag (1.0 / diag_A )
840847 M_dp = dpnp .asarray (M_np )
@@ -871,9 +878,9 @@ def test_cg_gmres_minres_agree_spd(self, n):
871878
872879 assert info_cg == 0 and info_gm == 0 and info_mr == 0
873880
874- assert_allclose (numpy . asarray (x_cg ), numpy . asarray (x_gm ), rtol = 1e-5 ,
881+ assert_allclose (_to_numpy (x_cg ), _to_numpy (x_gm ), rtol = 1e-5 ,
875882 err_msg = "CG and GMRES disagree" )
876- assert_allclose (numpy . asarray (x_cg ), numpy . asarray (x_mr ), rtol = 1e-5 ,
883+ assert_allclose (_to_numpy (x_cg ), _to_numpy (x_mr ), rtol = 1e-5 ,
877884 err_msg = "CG and MINRES disagree" )
878885
879886 def test_all_solvers_vs_numpy_direct (self ):
@@ -892,7 +899,7 @@ def test_all_solvers_vs_numpy_direct(self):
892899
893900 for name , x_dp in [("cg" , x_cg ), ("gmres" , x_gm ), ("minres" , x_mr )]:
894901 assert_allclose (
895- numpy . asarray (x_dp ), x_ref , rtol = 1e-7 ,
902+ _to_numpy (x_dp ), x_ref , rtol = 1e-7 ,
896903 err_msg = f"{ name } deviates from numpy.linalg.solve"
897904 )
898905
0 commit comments