@@ -35,6 +35,7 @@ same Meson, Ninja, and SciPy versions used by the dedicated BLAS/LAPACK CI lane
3535before running this example:
3636
3737``` console
38+ sudo apt-get install libblas-dev liblapack-dev
3839python3 -m pip install "meson==1.11.2" "ninja==1.13.0" "scipy==1.18.0"
3940```
4041
@@ -47,8 +48,9 @@ It performs these operations once:
4748 cached shared library;
48493 . build one PRIK extension from the complete generated contract and shared
4950 library;
50- 4 . build one raw f2py comparison surface for the reviewed 127 routines, linked
51- to that same complete native library; and
51+ 4 . independently build one raw f2py comparison surface from the 125 reviewed
52+ f2py-compatible routine sources and their minimal module dependency, linking unselected
53+ LAPACK and BLAS helper symbols from the system development libraries; and
52545 . reuse the three imported comparison surfaces for every correctness file.
5355
5456The contract-generation command is:
@@ -63,28 +65,52 @@ f2py command assembled by `conftest.py` is equivalent to:
6365
6466``` console
6567python3 -m numpy.f2py -c -m f2py_reference_lapack_example \
66- <the 127 source files recorded in routine_inventory.py> \
67- <the cached complete libprik_full_lapack.so> \
68+ examples/lapack/native/la_constants.f90 \
69+ <the 125 f2py-compatible source files recorded in routine_inventory.py> \
70+ only: <the 125 f2py-compatible routine names> : \
71+ --dep lapack --dep blas \
72+ --f2cmap /tmp/prik-lapack/.f2py_f2cmap \
6873 --build-dir /tmp/prik-lapack/f2py --f77flags=-O0 --f90flags=-O0 --opt=-O0
6974```
7075
76+ ` dlartg.f90 ` imports the ` LA_CONSTANTS ` module and declares its arguments as
77+ ` REAL(wp) ` . The f2py build therefore includes ` la_constants.f90 ` as a build-only
78+ dependency and supplies a kind map that resolves ` wp ` to C ` double ` . The
79+ ` only: ` list makes the 125 f2py-compatible routine wrappers explicit; module support metadata
80+ is not counted as a selected routine. The dependency is compiled only to
81+ satisfy f2py's standalone selected-source build. f2py does not compile or link
82+ PRIK's complete 2,062-source native artifact. The reviewed implementations come
83+ from ` examples/lapack/native/ ` ; only their unselected transitive LAPACK and BLAS
84+ dependencies come from the system libraries.
85+
7186Build products stay in pytest temporary/cache directories. Failures report the
7287compiler identity, command, stdout, and stderr. Neither build dirties the
7388repository.
7489
7590The fixtures import the complete PRIK module, the raw f2py module, and
7691` scipy.linalg.lapack ` . Character flags are Python ` str ` through PRIK and
77- ` bytes ` through SciPy/f2py. PRIK and raw f2py retain native argument ordering;
78- SciPy projects arrays and optional arguments into its documented Python API.
92+ ` bytes ` through SciPy/f2py. PRIK retains the complete native argument order.
93+ Raw f2py retains the order of the arguments it exposes, but infers and hides
94+ leading dimensions and a few other shape-only scalar arguments; each visible
95+ f2py call follows the generated wrapper signature. SciPy projects arrays and
96+ optional arguments into its documented Python API.
7997
8098The Reference LAPACK subroutines do not declare Fortran ` intent ` , so raw f2py
8199does not project their scalar writebacks. It still exposes in-place array
82- mutation, which provides enough output for the independent oracle in 114 of
83- the 123 subroutines; the four LAPACK functions return their values normally. Nine
84- subroutines have an essential scalar-only result, or need an unprojected scalar
85- to validate their mutated vector, and are explicitly recorded in
86- ` F2PY_NUMERICAL_LIMITATIONS ` . All 127 selected names must still be exported by
87- the one f2py comparison module.
100+ mutation, which provides enough output for the independent oracle in 112 of
101+ the 121 exported subroutines; the four exported LAPACK functions return their
102+ values normally. Nine subroutines have an essential scalar-only result, or need
103+ an unprojected scalar to validate their mutated vector, and are explicitly
104+ recorded in ` F2PY_NUMERICAL_LIMITATIONS ` .
105+
106+ ` dgees ` and ` dgges ` remain in the 127-routine correctness inventory but are
107+ recorded in ` F2PY_EXPORT_LIMITATIONS ` . Their Reference LAPACK interfaces accept
108+ external selection callbacks. From the unannotated implementation sources,
109+ NumPy f2py 2.5.1 generates incomplete callback declarations (` select_t ` and
110+ ` selctg_t ` ) and invalid C wrapper code. Those two routines are therefore
111+ validated through PRIK, SciPy, and independent Schur reconstruction without a
112+ raw-f2py call; the remaining 125 selected names must be exported by the one
113+ f2py comparison module.
88114
89115The nine reviewed limitations are:
90116
@@ -109,15 +135,28 @@ pivot ties may choose another valid permutation. Tests therefore use residuals,
109135subspaces, factor reconstruction, and orthogonality instead of byte equality
110136where the contract permits freedom.
111137
112- All numerical inputs use ` dtype=np.float64 ` . ` assert_allclose_float64 ` scales
113- tolerance with double-precision epsilon and operation length. Residual checks
114- also scale by matrix and solution norms. Integer results, ` INFO ` , shapes, dtypes,
115- pivot bounds, and untouched sentinel storage use exact comparisons.
138+ All numerical arrays use ` dtype=np.float64 ` . PRIK exposes the native scalar
139+ contract exactly, so its visible calls use ` np.int32(...) ` for LAPACK ` INTEGER `
140+ arguments, ` np.float64(...) ` for scalar ` DOUBLE PRECISION ` arguments, and
141+ ` np.bool_(...) ` for scalar ` LOGICAL ` arguments. SciPy and f2py calls retain their
142+ own accepted Python conventions beside the PRIK call. ` assert_allclose_float64 `
143+ scales tolerance with double-precision epsilon and operation length. Residual
144+ checks also scale by matrix and solution norms. Integer results, ` INFO ` , shapes,
145+ dtypes, pivot bounds, and untouched sentinel storage use exact comparisons.
146+
147+ ` DTGSEN ` and ` DTRSEN ` accept default-Fortran ` LOGICAL ` selection arrays. The
148+ pinned GFortran ABI stores each such element in four bytes, while PRIK's Python
149+ surface accepts a NumPy bool buffer. ` gfortran_logical_mask ` makes that genuine
150+ ABI difference explicit by placing each truth byte at the start of its native
151+ four-byte cell. The two affected calls remain visible, independently validate
152+ the requested reorder, and are recorded in ` PRIK_ABI_ADAPTERS ` rather than
153+ silently treating an ordinary two-byte NumPy mask as native storage.
116154
117155## Arrays, storage, workspaces, and indexes
118156
119157Matrices are explicitly Fortran contiguous where the native contract requires
120- column-major storage. Leading dimensions remain visible in PRIK/f2py calls.
158+ column-major storage. Leading dimensions remain visible in PRIK calls and are
159+ omitted only where the generated f2py signature explicitly infers them.
121160Tests use NaNs or sentinel values in unused triangles and padding, then prove
122161that those locations remain untouched. Helpers reconstruct general band,
123162symmetric band, tridiagonal, packed, triangular, and rectangular-full-packed
@@ -130,9 +169,11 @@ Invalid argument calls that could route through `XERBLA` are not made in-process
130169
131170Native LAPACK pivots and positions are one-based. PRIK and the raw f2py surface
132171preserve native values. SciPy converts several low-level pivot/index APIs to
133- zero-based values; each affected test performs the conversion beside the call.
134- The inventory marks every pivot/index-bearing routine so a new conversion
135- cannot be hidden in a generic adapter.
172+ zero-based values, but preserves one-based ` JPVT ` for ` DGELSY ` /` DGEQP3 ` , one-based
173+ ` IPIV ` for the general-tridiagonal ` DGTTRF ` /` DGTTRS ` family, and one-based Schur
174+ reorder positions for ` DTGEXC ` /` DTREXC ` . Each affected test keeps the observed
175+ convention beside the call. The inventory marks every pivot/index-bearing
176+ routine so a new conversion cannot be hidden in a generic adapter.
136177
137178## Run and diagnose
138179
@@ -173,9 +214,11 @@ documented totals.
173214| Explicit correctness tests | 127 |
174215| PRIK exports required in CI | 127 |
175216| SciPy exports used | 127 |
176- | f2py exports required in CI | 127 |
177- | Routines satisfying the independent oracle through f2py | 118 |
217+ | f2py exports required in CI | 125 |
218+ | Routines satisfying the independent oracle through f2py | 116 |
219+ | Documented raw-f2py export limitations | 2 |
178220| Documented raw-f2py numerical projection limitations | 9 |
221+ | Documented PRIK default-LOGICAL ABI adapters | 2 |
179222| Documented unsupported/skipped routines | 0 |
180223
181224Runtime success, f2py projection limitations, and any PRIK failures are CI
@@ -200,8 +243,10 @@ def test_dgesv_solves_general_system(prik_lapack, scipy_lapack, f2py_lapack):
200243 prik_piv = np.empty(2 , dtype = np.int32)
201244 f2py_piv = np.empty(2 , dtype = np.int32)
202245
203- prik_scalars = prik_lapack.dgesv(2 , 1 , prik_a, 2 , prik_piv, prik_b, 2 , 0 )
204- f2py_result = f2py_lapack.dgesv(2 , 1 , f2py_a, 2 , f2py_piv, f2py_b, 2 , 0 )
246+ prik_scalars = prik_lapack.dgesv(
247+ np.int32(2 ), np.int32(1 ), prik_a, np.int32(2 ), prik_piv, prik_b, np.int32(2 ), np.int32(0 )
248+ )
249+ f2py_result = f2py_lapack.dgesv(2 , 1 , f2py_a, f2py_piv, f2py_b, 0 )
205250 scipy_lu, scipy_piv, scipy_x, scipy_info = scipy_lapack.dgesv(
206251 original_a.copy(order = " F" ), original_b.copy(order = " F" )
207252 )
@@ -234,8 +279,8 @@ def test_dgetrf_reconstructs_pivoted_lu(prik_lapack, scipy_lapack, f2py_lapack):
234279 prik_piv = np.empty(2 , dtype = np.int32)
235280 f2py_piv = np.empty(2 , dtype = np.int32)
236281
237- prik_scalars = prik_lapack.dgetrf(2 , 2 , prik_a, 2 , prik_piv, 0 )
238- f2py_result = f2py_lapack.dgetrf(2 , 2 , f2py_a, 2 , f2py_piv, 0 )
282+ prik_scalars = prik_lapack.dgetrf(np.int32( 2 ), np.int32( 2 ) , prik_a, np.int32( 2 ) , prik_piv, np.int32( 0 ) )
283+ f2py_result = f2py_lapack.dgetrf(2 , 2 , f2py_a, f2py_piv, 0 )
239284 scipy_lu, scipy_piv, scipy_info = scipy_lapack.dgetrf(original.copy(order = " F" ))
240285
241286 assert prik_scalars == (2 , 2 , 2 , 0 )
@@ -260,8 +305,8 @@ def test_dpotrf_reconstructs_spd_matrix(prik_lapack, scipy_lapack, f2py_lapack):
260305 stored = np.array([[4.0 , np.nan], [1.0 , 3.0 ]], dtype = np.float64, order = " F" )
261306 prik_a, f2py_a = column_major(stored), column_major(stored)
262307
263- prik_scalars = prik_lapack.dpotrf(" L" , 2 , prik_a, 2 , 0 )
264- f2py_result = f2py_lapack.dpotrf(b " L" , 2 , f2py_a, 2 , 0 )
308+ prik_scalars = prik_lapack.dpotrf(" L" , np.int32( 2 ) , prik_a, np.int32( 2 ), np.int32( 0 ) )
309+ f2py_result = f2py_lapack.dpotrf(b " L" , 2 , f2py_a, 0 )
265310 scipy_factor, scipy_info = scipy_lapack.dpotrf(stored.copy(order = " F" ), lower = 1 , clean = 0 )
266311
267312 assert prik_scalars == (2 , 2 , 0 )
@@ -289,8 +334,10 @@ def test_dgeqrf_reconstructs_qr_factorization(prik_lapack, scipy_lapack, f2py_la
289334 prik_tau = np.empty(2 , dtype = np.float64)
290335 f2py_tau = np.empty(2 , dtype = np.float64)
291336
292- prik_scalars = prik_lapack.dgeqrf(3 , 2 , prik_a, 3 , prik_tau, np.empty(16 ), 16 , 0 )
293- f2py_result = f2py_lapack.dgeqrf(3 , 2 , f2py_a, 3 , f2py_tau, np.empty(16 ), 16 , 0 )
337+ prik_scalars = prik_lapack.dgeqrf(
338+ np.int32(3 ), np.int32(2 ), prik_a, np.int32(3 ), prik_tau, np.empty(16 ), np.int32(16 ), np.int32(0 )
339+ )
340+ f2py_result = f2py_lapack.dgeqrf(3 , 2 , f2py_a, f2py_tau, np.empty(16 ), 16 , 0 )
294341 scipy_qr, scipy_tau, _scipy_work, scipy_info = scipy_lapack.dgeqrf(matrix.copy(order = " F" ), lwork = 16 )
295342
296343 assert prik_scalars == (3 , 2 , 3 , 16 , 0 )
@@ -318,8 +365,10 @@ def test_dsyev_returns_orthonormal_eigenvectors(prik_lapack, scipy_lapack, f2py_
318365 prik_w = np.empty(2 , dtype = np.float64)
319366 f2py_w = np.empty(2 , dtype = np.float64)
320367
321- prik_scalars = prik_lapack.dsyev(" V" , " U" , 2 , prik_vectors, 2 , prik_w, np.empty(16 ), 16 , 0 )
322- f2py_result = f2py_lapack.dsyev(b " V" , b " U" , 2 , f2py_vectors, 2 , f2py_w, np.empty(16 ), 16 , 0 )
368+ prik_scalars = prik_lapack.dsyev(
369+ " V" , " U" , np.int32(2 ), prik_vectors, np.int32(2 ), prik_w, np.empty(16 ), np.int32(16 ), np.int32(0 )
370+ )
371+ f2py_result = f2py_lapack.dsyev(b " V" , b " U" , 2 , f2py_vectors, f2py_w, np.empty(16 ), 16 , 0 )
323372 scipy_w, scipy_vectors, scipy_info = scipy_lapack.dsyev(matrix.copy(order = " F" ), compute_v = 1 , lower = 0 , lwork = 16 )
324373
325374 assert prik_scalars == (2 , 2 , 16 , 0 )
@@ -346,8 +395,23 @@ def test_dgesvd_reconstructs_matrix(prik_lapack, scipy_lapack, f2py_lapack):
346395 prik_u, f2py_u = column_major(np.zeros((3 , 3 ))), column_major(np.zeros((3 , 3 )))
347396 prik_vt, f2py_vt = column_major(np.zeros((2 , 2 ))), column_major(np.zeros((2 , 2 )))
348397
349- prik_scalars = prik_lapack.dgesvd(" A" , " A" , 3 , 2 , prik_a, 3 , prik_s, prik_u, 3 , prik_vt, 2 , np.empty(32 ), 32 , 0 )
350- f2py_result = f2py_lapack.dgesvd(b " A" , b " A" , 3 , 2 , f2py_a, 3 , f2py_s, f2py_u, 3 , f2py_vt, 2 , np.empty(32 ), 32 , 0 )
398+ prik_scalars = prik_lapack.dgesvd(
399+ " A" ,
400+ " A" ,
401+ np.int32(3 ),
402+ np.int32(2 ),
403+ prik_a,
404+ np.int32(3 ),
405+ prik_s,
406+ prik_u,
407+ np.int32(3 ),
408+ prik_vt,
409+ np.int32(2 ),
410+ np.empty(32 ),
411+ np.int32(32 ),
412+ np.int32(0 ),
413+ )
414+ f2py_result = f2py_lapack.dgesvd(b " A" , b " A" , 3 , 2 , f2py_a, f2py_s, f2py_u, f2py_vt, np.empty(32 ), 32 , 0 )
351415 scipy_u, scipy_s, scipy_vt, scipy_info = scipy_lapack.dgesvd(
352416 matrix.copy(order = " F" ), compute_uv = 1 , full_matrices = 1 , lwork = 32
353417 )
@@ -379,8 +443,10 @@ def test_dtbtrs_solves_triangular_band_system(prik_lapack, scipy_lapack, f2py_la
379443 expected = np.array([[1.0 ], [2.0 ]], dtype = np.float64)
380444 prik_b, f2py_b = rhs.copy(order = " F" ), rhs.copy(order = " F" )
381445
382- prik_scalars = prik_lapack.dtbtrs(" U" , " N" , " N" , 2 , 1 , 1 , band, 2 , prik_b, 2 , 0 )
383- f2py_result = f2py_lapack.dtbtrs(b " U" , b " N" , b " N" , 2 , 1 , 1 , band, 2 , f2py_b, 2 , 0 )
446+ prik_scalars = prik_lapack.dtbtrs(
447+ " U" , " N" , " N" , np.int32(2 ), np.int32(1 ), np.int32(1 ), band, np.int32(2 ), prik_b, np.int32(2 ), np.int32(0 )
448+ )
449+ f2py_result = f2py_lapack.dtbtrs(b " U" , b " N" , b " N" , 2 , 1 , 1 , band, f2py_b, 0 )
384450 scipy_x, scipy_info = scipy_lapack.dtbtrs(band, rhs.copy(order = " F" ), uplo = b " U" , trans = b " N" , diag = b " N" )
385451
386452 assert f2py_result is None
@@ -400,10 +466,18 @@ def test_dgecon_estimates_reciprocal_condition(prik_lapack, scipy_lapack, f2py_l
400466 expected = 1.0
401467
402468 prik_scalars = prik_lapack.dgecon(
403- " 1" , 1 , factor.copy(order = " F" ), 1 , 4.0 , 0.0 , np.empty(4 ), np.empty(1 , dtype = np.int32), 0
469+ " 1" ,
470+ np.int32(1 ),
471+ factor.copy(order = " F" ),
472+ np.int32(1 ),
473+ np.float64(4.0 ),
474+ np.float64(0.0 ),
475+ np.empty(4 ),
476+ np.empty(1 , dtype = np.int32),
477+ np.int32(0 ),
404478 )
405479 f2py_result = f2py_lapack.dgecon(
406- b " 1" , 1 , factor.copy(order = " F" ), 1 , 4.0 , 0.0 , np.empty(4 ), np.empty(1 , dtype = np.int32), 0
480+ b " 1" , 1 , factor.copy(order = " F" ), 4.0 , 0.0 , np.empty(4 ), np.empty(1 , dtype = np.int32), 0
407481 )
408482 scipy_rcond, scipy_info = scipy_lapack.dgecon(factor.copy(order = " F" ), 4.0 , norm = b " 1" )
409483
0 commit comments