Coverage for jetgp/full_degp_sparse/degp_utils.py: 39%
440 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-04-08 20:49 -0500
« prev ^ index » next coverage.py v7.10.7, created at 2026-04-08 20:49 -0500
1import numpy as np
2import pyoti.core as coti
3from line_profiler import profile
4import numba
7# =============================================================================
8# Numba-accelerated helper functions for efficient matrix slicing
9# =============================================================================
11@numba.jit(nopython=True, cache=True)
12def extract_rows(content_full, row_indices, n_cols):
13 """
14 Extract rows from content_full at specified indices.
16 Parameters
17 ----------
18 content_full : ndarray of shape (n_rows_full, n_cols)
19 Source matrix.
20 row_indices : ndarray of int64
21 Row indices to extract.
22 n_cols : int
23 Number of columns.
25 Returns
26 -------
27 result : ndarray of shape (len(row_indices), n_cols)
28 Extracted rows.
29 """
30 n_rows = len(row_indices)
31 result = np.empty((n_rows, n_cols))
32 for i in range(n_rows):
33 ri = row_indices[i]
34 for j in range(n_cols):
35 result[i, j] = content_full[ri, j]
36 return result
39@numba.jit(nopython=True, cache=True)
40def extract_cols(content_full, col_indices, n_rows):
41 """
42 Extract columns from content_full at specified indices.
44 Parameters
45 ----------
46 content_full : ndarray of shape (n_rows, n_cols_full)
47 Source matrix.
48 col_indices : ndarray of int64
49 Column indices to extract.
50 n_rows : int
51 Number of rows.
53 Returns
54 -------
55 result : ndarray of shape (n_rows, len(col_indices))
56 Extracted columns.
57 """
58 n_cols = len(col_indices)
59 result = np.empty((n_rows, n_cols))
60 for i in range(n_rows):
61 for j in range(n_cols):
62 result[i, j] = content_full[i, col_indices[j]]
63 return result
66@numba.jit(nopython=True, cache=True)
67def extract_submatrix(content_full, row_indices, col_indices):
68 """
69 Extract submatrix from content_full at specified row and column indices.
70 Replaces the expensive np.ix_ operation.
72 Parameters
73 ----------
74 content_full : ndarray of shape (n_rows_full, n_cols_full)
75 Source matrix.
76 row_indices : ndarray of int64
77 Row indices to extract.
78 col_indices : ndarray of int64
79 Column indices to extract.
81 Returns
82 -------
83 result : ndarray of shape (len(row_indices), len(col_indices))
84 Extracted submatrix.
85 """
86 n_rows = len(row_indices)
87 n_cols = len(col_indices)
88 result = np.empty((n_rows, n_cols))
89 for i in range(n_rows):
90 ri = row_indices[i]
91 for j in range(n_cols):
92 result[i, j] = content_full[ri, col_indices[j]]
93 return result
96@numba.jit(nopython=True, cache=True, parallel=False)
97def extract_and_assign(content_full, row_indices, col_indices, K,
98 row_start, col_start, sign):
99 """
100 Extract submatrix and assign directly to K with sign multiplication.
101 Combines extraction and assignment in one pass for better performance.
103 Parameters
104 ----------
105 content_full : ndarray of shape (n_rows_full, n_cols_full)
106 Source matrix.
107 row_indices : ndarray of int64
108 Row indices to extract.
109 col_indices : ndarray of int64
110 Column indices to extract.
111 K : ndarray
112 Target matrix to fill.
113 row_start : int
114 Starting row index in K.
115 col_start : int
116 Starting column index in K.
117 sign : float
118 Sign multiplier (+1.0 or -1.0).
119 """
120 n_rows = len(row_indices)
121 n_cols = len(col_indices)
122 for i in range(n_rows):
123 ri = row_indices[i]
124 for j in range(n_cols):
125 K[row_start + i, col_start + j] = content_full[ri, col_indices[j]] * sign
128@numba.jit(nopython=True, cache=True)
129def extract_rows_and_assign(content_full, row_indices, K,
130 row_start, col_start, n_cols, sign):
131 """
132 Extract rows and assign directly to K with sign multiplication.
134 Parameters
135 ----------
136 content_full : ndarray of shape (n_rows_full, n_cols)
137 Source matrix.
138 row_indices : ndarray of int64
139 Row indices to extract.
140 K : ndarray
141 Target matrix to fill.
142 row_start : int
143 Starting row index in K.
144 col_start : int
145 Starting column index in K.
146 n_cols : int
147 Number of columns to copy.
148 sign : float
149 Sign multiplier (+1.0 or -1.0).
150 """
151 n_rows = len(row_indices)
152 for i in range(n_rows):
153 ri = row_indices[i]
154 for j in range(n_cols):
155 K[row_start + i, col_start + j] = content_full[ri, j] * sign
158@numba.jit(nopython=True, cache=True)
159def extract_cols_and_assign(content_full, col_indices, K,
160 row_start, col_start, n_rows, sign):
161 """
162 Extract columns and assign directly to K with sign multiplication.
164 Parameters
165 ----------
166 content_full : ndarray of shape (n_rows, n_cols_full)
167 Source matrix.
168 col_indices : ndarray of int64
169 Column indices to extract.
170 K : ndarray
171 Target matrix to fill.
172 row_start : int
173 Starting row index in K.
174 col_start : int
175 Starting column index in K.
176 n_rows : int
177 Number of rows to copy.
178 sign : float
179 Sign multiplier (+1.0 or -1.0).
180 """
181 n_cols = len(col_indices)
182 for i in range(n_rows):
183 for j in range(n_cols):
184 K[row_start + i, col_start + j] = content_full[i, col_indices[j]] * sign
187# =============================================================================
188# Difference computation functions
189# =============================================================================
192def differences_by_dim_func(X1, X2, n_order, oti_module, return_deriv=True):
193 """
194 Compute pairwise differences between two input arrays X1 and X2 for each dimension,
195 embedding hypercomplex units along each dimension for automatic differentiation.
197 For each dimension k, this function computes:
198 diff_k[i, j] = X1[i, k] + e_{k+1} - X2[j, k]
199 where e_{k+1} is a hypercomplex unit for the (k+1)-th dimension with order 2 * n_order.
201 Parameters
202 ----------
203 X1 : array_like of shape (n1, d)
204 First set of input points with n1 samples in d dimensions.
205 X2 : array_like of shape (n2, d)
206 Second set of input points with n2 samples in d dimensions.
207 n_order : int
208 The base order used to construct hypercomplex units (e_{k+1}) with order 2 * n_order.
209 oti_module : module
210 The PyOTI static module (e.g., pyoti.static.onumm4n2).
211 return_deriv : bool, optional
212 If True, use 2*n_order for derivative predictions.
214 Returns
215 -------
216 differences_by_dim : list of length d
217 A list where each element is an array of shape (n1, n2), containing the differences
218 between corresponding dimensions of X1 and X2, augmented with hypercomplex units.
219 """
220 # Keep numpy copies for fused path
221 X1_np = np.asarray(X1, dtype=np.float64)
222 X2_np = np.asarray(X2, dtype=np.float64)
223 n1, d = X1_np.shape
224 n2 = X2_np.shape[0]
226 # Check if the fused C-level function is available
227 _use_fused = hasattr(oti_module.zeros((1, 1)), 'fused_from_real_with_perturbations')
229 if _use_fused:
230 # --- Fused path: numpy broadcast for real part, C-level OTI fill ---
231 differences_by_dim = []
232 perturb2 = oti_module.zeros((n2, 1)) # X2 has no perturbation in DEGP
234 if n_order == 0:
235 # No perturbation needed — just real differences
236 perturb1 = oti_module.zeros((n1, 1))
237 for k in range(d):
238 real_diffs = np.ascontiguousarray(
239 X1_np[:, k:k+1] - X2_np[:, k:k+1].T, dtype=np.float64
240 )
241 diffs_k = oti_module.empty((n1, n2))
242 diffs_k.fused_from_real_with_perturbations(real_diffs, perturb1, perturb2)
243 differences_by_dim.append(diffs_k)
244 else:
245 if return_deriv:
246 hc_order = 2 * n_order
247 else:
248 hc_order = n_order
250 for k in range(d):
251 # Real differences via numpy broadcasting (fast)
252 real_diffs = np.ascontiguousarray(
253 X1_np[:, k:k+1] - X2_np[:, k:k+1].T, dtype=np.float64
254 )
255 # Perturbation: e_{k+1} broadcast to all n1 points
256 perturb1 = oti_module.zeros((n1, 1)) + oti_module.e(k + 1, order=hc_order)
257 # Fused fill: out[i,j].real = real_diffs[i,j], out[i,j].im = perturb1[i] - 0
258 diffs_k = oti_module.empty((n1, n2))
259 diffs_k.fused_from_real_with_perturbations(real_diffs, perturb1, perturb2)
260 differences_by_dim.append(diffs_k)
262 return differences_by_dim
264 # --- Fallback: original Python loop path ---
265 X1 = oti_module.array(X1_np)
266 X2 = oti_module.array(X2_np)
268 differences_by_dim = []
270 if n_order == 0:
271 for k in range(d):
272 diffs_k = oti_module.zeros((n1, n2))
273 for i in range(n1):
274 diffs_k[i, :] = X1[i, k] - (oti_module.transpose(X2[:, k]))
275 differences_by_dim.append(diffs_k)
276 elif not return_deriv:
277 for k in range(d):
278 diffs_k = oti_module.zeros((n1, n2))
279 for i in range(n1):
280 diffs_k[i, :] = (
281 X1[i, k]
282 + oti_module.e(k + 1, order=n_order)
283 - (X2[:, k].T)
284 )
285 differences_by_dim.append(diffs_k)
286 else:
287 for k in range(d):
288 diffs_k = oti_module.zeros((n1, n2))
289 for i in range(n1):
290 diffs_k[i, :] = X1[i, k] - (X2[:, k].T)
291 differences_by_dim.append(
292 diffs_k + oti_module.e(k + 1, order=2 * n_order))
294 return differences_by_dim
297# =============================================================================
298# Derivative mapping utilities
299# =============================================================================
301def deriv_map(nbases, order):
302 """
303 Create mapping from (order, index) to flattened index.
305 Parameters
306 ----------
307 nbases : int
308 Number of base dimensions.
309 order : int
310 Maximum derivative order.
312 Returns
313 -------
314 map_deriv : list of lists
315 Mapping where map_deriv[order][idx] gives the flattened index.
316 """
317 k = 0
318 map_deriv = []
319 for ordi in range(order + 1):
320 ndir = coti.ndir_order(nbases, ordi)
321 map_deriv_i = [0] * ndir
322 for idx in range(ndir):
323 map_deriv_i[idx] = k
324 k += 1
325 map_deriv.append(map_deriv_i)
326 return map_deriv
329def transform_der_indices(der_indices, der_map):
330 """
331 Transform derivative indices to flattened format.
333 Parameters
334 ----------
335 der_indices : list
336 User-facing derivative specifications.
337 der_map : list of lists
338 Derivative mapping from deriv_map().
340 Returns
341 -------
342 deriv_ind_transf : list
343 Flattened indices for each derivative.
344 deriv_ind_order : list
345 (index, order) tuples for each derivative.
346 """
347 deriv_ind_transf = []
348 deriv_ind_order = []
349 for deriv in der_indices:
350 imdir = coti.imdir(deriv)
351 idx, order = imdir
352 deriv_ind_transf.append(der_map[order][idx])
353 deriv_ind_order.append(imdir)
354 return deriv_ind_transf, deriv_ind_order
357# =============================================================================
358# RBF Kernel Assembly Functions (Optimized with Numba)
359# =============================================================================
361# @profile
362def rbf_kernel(
363 phi,
364 phi_exp,
365 n_order,
366 n_bases,
367 der_indices,
368 powers,
369 index=None
370):
371 """
372 Compute the derivative-enhanced RBF kernel matrix (optimized version).
374 This version uses Numba-accelerated functions for efficient matrix slicing,
375 replacing expensive np.ix_ operations.
377 Parameters
378 ----------
379 phi : OTI array
380 Base kernel matrix from kernel_func(differences, length_scales).
381 phi_exp : ndarray
382 Expanded derivative array from phi.get_all_derivs().
383 n_order : int
384 Maximum derivative order considered.
385 n_bases : int
386 Number of input dimensions.
387 der_indices : list of lists
388 Multi-index derivative structures for each derivative component.
389 powers : list of int
390 Powers of (-1) applied to each term.
391 index : list of lists or None, optional
392 If empty list, assumes uniform blocks.
393 If provided, specifies which training point indices have each derivative type.
395 Returns
396 -------
397 K : ndarray
398 Kernel matrix including function values and derivative terms.
399 """
400 dh = coti.get_dHelp()
402 n_rows_func, n_cols_func = phi.shape
403 n_deriv_types = len(der_indices)
405 der_map = deriv_map(n_bases, 2 * n_order)
406 der_indices_tr, der_ind_order = transform_der_indices(der_indices, der_map)
408 # Pre-compute signs (avoid repeated exponentiation)
409 signs = np.array([(-1.0) ** p for p in powers], dtype=np.float64)
411 # =========================================================================
412 # CASE 1: Uniform blocks (original behavior) - index is None or empty
413 # =========================================================================
414 if index is None or len(index) == 0:
415 K = np.zeros((n_rows_func * (n_deriv_types + 1),
416 n_cols_func * (n_deriv_types + 1)))
417 outer_loop_index = n_deriv_types + 1
419 for j in range(outer_loop_index):
420 signj = signs[j]
421 for i in range(n_deriv_types + 1):
422 Klocal = K[i * n_rows_func: (i + 1) * n_rows_func,
423 j * n_cols_func: (j + 1) * n_cols_func]
424 if j == 0 and i == 0:
425 Klocal[:, :] = phi_exp[0] * signj
427 return K
429 # =========================================================================
430 # CASE 2: Non-contiguous indices - index is provided
431 # =========================================================================
432 n_pts_with_derivs_rows = sum(len(order_indices) for order_indices in index)
433 total_rows = n_rows_func + n_pts_with_derivs_rows
434 n_pts_with_derivs_cols = sum(len(order_indices) for order_indices in index)
435 total_cols = n_cols_func + n_pts_with_derivs_cols
437 K = np.zeros((total_rows, total_cols))
438 base_shape = (n_rows_func, n_cols_func)
440 # Convert index lists to numpy arrays for numba
441 index_arrays = [np.asarray(idx, dtype=np.int64) for idx in index]
443 # Block (0,0): Function-Function (K_ff)
444 content_full = phi_exp[0].reshape(base_shape)
445 K[:n_rows_func, :n_cols_func] = content_full * signs[0]
447 # First Block-Column: Derivative-Function (K_df)
448 row_offset = n_rows_func
449 for i in range(n_deriv_types):
450 flat_idx = der_indices_tr[i]
451 content_full = phi_exp[flat_idx].reshape(base_shape)
452 row_indices = index_arrays[i]
453 n_pts_this_order = len(row_indices)
455 # Use numba for efficient row extraction and assignment
456 extract_rows_and_assign(content_full, row_indices, K,
457 row_offset, 0, n_cols_func, signs[0])
458 row_offset += n_pts_this_order
460 # First Block-Row: Function-Derivative (K_fd)
461 col_offset = n_cols_func
462 for j in range(n_deriv_types):
463 flat_idx = der_indices_tr[j]
464 content_full = phi_exp[flat_idx].reshape(base_shape)
465 col_indices = index_arrays[j]
466 n_pts_this_order = len(col_indices)
468 # Use numba for efficient column extraction and assignment
469 extract_cols_and_assign(content_full, col_indices, K,
470 0, col_offset, n_rows_func, signs[j + 1])
471 col_offset += n_pts_this_order
473 # Inner Blocks: Derivative-Derivative (K_dd)
474 row_offset = n_rows_func
475 for i in range(n_deriv_types):
476 col_offset = n_cols_func
477 row_indices = index_arrays[i]
478 n_pts_row = len(row_indices)
480 for j in range(n_deriv_types):
481 col_indices = index_arrays[j]
482 n_pts_col = len(col_indices)
484 imdir1 = der_ind_order[j]
485 imdir2 = der_ind_order[i]
486 new_idx, new_ord = dh.mult_dir(
487 imdir1[0], imdir1[1], imdir2[0], imdir2[1])
488 flat_idx = der_map[new_ord][new_idx]
489 content_full = phi_exp[flat_idx].reshape(base_shape)
491 # Use numba for direct extraction and assignment (replaces np.ix_)
492 extract_and_assign(content_full, row_indices, col_indices, K,
493 row_offset, col_offset, signs[j + 1])
495 col_offset += n_pts_col
496 row_offset += n_pts_row
498 return K
501@numba.jit(nopython=True, cache=True)
502def _assemble_kernel_numba(phi_exp_3d, K, n_rows_func, n_cols_func,
503 fd_flat_indices, df_flat_indices, dd_flat_indices,
504 idx_flat, idx_offsets, idx_sizes,
505 signs, n_deriv_types, row_offsets, col_offsets):
506 """
507 Fused numba kernel that assembles the entire K matrix in a single call.
508 Handles ff, fd, df, and dd blocks without Python-level loop overhead.
509 """
510 # Block (0,0): Function-Function
511 s0 = signs[0]
512 for r in range(n_rows_func):
513 for c in range(n_cols_func):
514 K[r, c] = phi_exp_3d[0, r, c] * s0
516 # First Block-Row: Function-Derivative (fd)
517 for j in range(n_deriv_types):
518 fi = fd_flat_indices[j]
519 sj = signs[j + 1]
520 co = col_offsets[j]
521 off_j = idx_offsets[j]
522 sz_j = idx_sizes[j]
523 for r in range(n_rows_func):
524 for k in range(sz_j):
525 ci = idx_flat[off_j + k]
526 K[r, co + k] = phi_exp_3d[fi, r, ci] * sj
528 # First Block-Column: Derivative-Function (df)
529 for i in range(n_deriv_types):
530 fi = df_flat_indices[i]
531 ro = row_offsets[i]
532 off_i = idx_offsets[i]
533 sz_i = idx_sizes[i]
534 for k in range(sz_i):
535 ri = idx_flat[off_i + k]
536 for c in range(n_cols_func):
537 K[ro + k, c] = phi_exp_3d[fi, ri, c] * s0
539 # Inner Blocks: Derivative-Derivative (dd)
540 for i in range(n_deriv_types):
541 ro = row_offsets[i]
542 off_i = idx_offsets[i]
543 sz_i = idx_sizes[i]
544 for j in range(n_deriv_types):
545 fi = dd_flat_indices[i, j]
546 sj = signs[j + 1]
547 co = col_offsets[j]
548 off_j = idx_offsets[j]
549 sz_j = idx_sizes[j]
550 for ki in range(sz_i):
551 ri = idx_flat[off_i + ki]
552 for kj in range(sz_j):
553 ci = idx_flat[off_j + kj]
554 K[ro + ki, co + kj] = phi_exp_3d[fi, ri, ci] * sj
557@numba.jit(nopython=True, cache=True)
558def _project_W_to_phi_space(W, W_proj, n_rows_func, n_cols_func,
559 fd_flat_indices, df_flat_indices, dd_flat_indices,
560 idx_flat, idx_offsets, idx_sizes,
561 signs, n_deriv_types, row_offsets, col_offsets):
562 """
563 Reverse of _assemble_kernel_numba: project W from K-space back into
564 phi_exp-space so that vdot(W, assemble(dphi_exp)) == vdot(W_proj, dphi_exp).
566 This allows computing gradient contributions without materialising the
567 full dK matrix for each hyperparameter dimension.
568 """
569 # Zero out W_proj
570 for d in range(W_proj.shape[0]):
571 for r in range(W_proj.shape[1]):
572 for c in range(W_proj.shape[2]):
573 W_proj[d, r, c] = 0.0
575 s0 = signs[0]
577 # ff block: K[r, c] = phi_exp[0, r, c] * s0
578 for r in range(n_rows_func):
579 for c in range(n_cols_func):
580 W_proj[0, r, c] += s0 * W[r, c]
582 # fd blocks: K[r, co+k] = phi_exp[fi, r, idx[k]] * sj
583 for j in range(n_deriv_types):
584 fi = fd_flat_indices[j]
585 sj = signs[j + 1]
586 co = col_offsets[j]
587 off_j = idx_offsets[j]
588 sz_j = idx_sizes[j]
589 for r in range(n_rows_func):
590 for k in range(sz_j):
591 ci = idx_flat[off_j + k]
592 W_proj[fi, r, ci] += sj * W[r, co + k]
594 # df blocks: K[ro+k, c] = phi_exp[fi, idx[k], c] * s0
595 for i in range(n_deriv_types):
596 fi = df_flat_indices[i]
597 ro = row_offsets[i]
598 off_i = idx_offsets[i]
599 sz_i = idx_sizes[i]
600 for k in range(sz_i):
601 ri = idx_flat[off_i + k]
602 for c in range(n_cols_func):
603 W_proj[fi, ri, c] += s0 * W[ro + k, c]
605 # dd blocks: K[ro+ki, co+kj] = phi_exp[dd_fi[i,j], idx_i[ki], idx_j[kj]] * sj
606 for i in range(n_deriv_types):
607 ro = row_offsets[i]
608 off_i = idx_offsets[i]
609 sz_i = idx_sizes[i]
610 for j in range(n_deriv_types):
611 fi = dd_flat_indices[i, j]
612 sj = signs[j + 1]
613 co = col_offsets[j]
614 off_j = idx_offsets[j]
615 sz_j = idx_sizes[j]
616 for ki in range(sz_i):
617 ri = idx_flat[off_i + ki]
618 for kj in range(sz_j):
619 ci = idx_flat[off_j + kj]
620 W_proj[fi, ri, ci] += sj * W[ro + ki, co + kj]
623@numba.jit(nopython=True, cache=True)
624def _project_alpha_to_phi_space(alpha_v, alpha_proj, n_rows_func, n_cols_func,
625 fd_flat_indices, df_flat_indices, dd_flat_indices,
626 idx_flat, idx_offsets, idx_sizes,
627 signs, n_deriv_types, row_offsets, col_offsets):
628 """
629 Project the rank-1 matrix alpha*alpha^T into phi_exp-space.
631 Same structure as _project_W_to_phi_space but computes
632 alpha_v[r]*alpha_v[c] on the fly instead of reading W[r,c].
633 """
634 # Zero out alpha_proj
635 for d in range(alpha_proj.shape[0]):
636 for r in range(alpha_proj.shape[1]):
637 for c in range(alpha_proj.shape[2]):
638 alpha_proj[d, r, c] = 0.0
640 s0 = signs[0]
642 # ff block: alpha_v[r] * alpha_v[c]
643 for r in range(n_rows_func):
644 ar = alpha_v[r]
645 for c in range(n_cols_func):
646 alpha_proj[0, r, c] += s0 * ar * alpha_v[c]
648 # fd blocks
649 for j in range(n_deriv_types):
650 fi = fd_flat_indices[j]
651 sj = signs[j + 1]
652 co = col_offsets[j]
653 off_j = idx_offsets[j]
654 sz_j = idx_sizes[j]
655 for r in range(n_rows_func):
656 ar = alpha_v[r]
657 for k in range(sz_j):
658 ci = idx_flat[off_j + k]
659 alpha_proj[fi, r, ci] += sj * ar * alpha_v[co + k]
661 # df blocks
662 for i in range(n_deriv_types):
663 fi = df_flat_indices[i]
664 ro = row_offsets[i]
665 off_i = idx_offsets[i]
666 sz_i = idx_sizes[i]
667 for k in range(sz_i):
668 ri = idx_flat[off_i + k]
669 ak = alpha_v[ro + k]
670 for c in range(n_cols_func):
671 alpha_proj[fi, ri, c] += s0 * ak * alpha_v[c]
673 # dd blocks
674 for i in range(n_deriv_types):
675 ro = row_offsets[i]
676 off_i = idx_offsets[i]
677 sz_i = idx_sizes[i]
678 for j in range(n_deriv_types):
679 fi = dd_flat_indices[i, j]
680 sj = signs[j + 1]
681 co = col_offsets[j]
682 off_j = idx_offsets[j]
683 sz_j = idx_sizes[j]
684 for ki in range(sz_i):
685 ri = idx_flat[off_i + ki]
686 aki = alpha_v[ro + ki]
687 for kj in range(sz_j):
688 ci = idx_flat[off_j + kj]
689 alpha_proj[fi, ri, ci] += sj * aki * alpha_v[co + kj]
692def precompute_kernel_plan(n_order, n_bases, der_indices, powers, index):
693 """
694 Precompute all structural information needed by rbf_kernel so it can be
695 reused across repeated calls with different phi_exp values.
697 Returns a dict containing flat indices, signs, index arrays, precomputed
698 offsets/sizes, and mult_dir results for the dd block.
699 """
700 dh = coti.get_dHelp()
701 der_map = deriv_map(n_bases, 2 * n_order)
702 der_indices_tr, der_ind_order = transform_der_indices(der_indices, der_map)
704 n_deriv_types = len(der_indices)
705 signs = np.array([(-1.0) ** p for p in powers], dtype=np.float64)
706 index_arrays = [np.asarray(idx, dtype=np.int64) for idx in index]
708 # Precompute sizes and offsets
709 index_sizes = np.array([len(idx) for idx in index_arrays], dtype=np.int64)
710 n_pts_with_derivs = int(index_sizes.sum())
712 # Pack all index arrays into a single flat array with offsets
713 idx_flat = np.concatenate(index_arrays) if n_deriv_types > 0 else np.array([], dtype=np.int64)
714 idx_offsets = np.zeros(n_deriv_types, dtype=np.int64)
715 for i in range(1, n_deriv_types):
716 idx_offsets[i] = idx_offsets[i - 1] + index_sizes[i - 1]
718 # Precompute row/col offsets in K for each deriv type
719 row_offsets = np.zeros(n_deriv_types, dtype=np.int64)
720 col_offsets = np.zeros(n_deriv_types, dtype=np.int64)
721 # Note: n_rows_func == n_cols_func for training kernel, but we store
722 # offsets relative to n_rows_func which is added at call time
723 cumsum = 0
724 for i in range(n_deriv_types):
725 row_offsets[i] = cumsum # relative to n_rows_func
726 col_offsets[i] = cumsum # relative to n_cols_func
727 cumsum += index_sizes[i]
729 # Precompute mult_dir results for dd blocks
730 dd_flat_indices = np.empty((n_deriv_types, n_deriv_types), dtype=np.int64)
731 for i in range(n_deriv_types):
732 for j in range(n_deriv_types):
733 imdir1 = der_ind_order[j]
734 imdir2 = der_ind_order[i]
735 new_idx, new_ord = dh.mult_dir(
736 imdir1[0], imdir1[1], imdir2[0], imdir2[1])
737 dd_flat_indices[i, j] = der_map[new_ord][new_idx]
739 # fd and df flat indices as arrays
740 fd_flat_indices = np.array(der_indices_tr, dtype=np.int64)
741 df_flat_indices = np.array(der_indices_tr, dtype=np.int64)
743 return {
744 'der_indices_tr': der_indices_tr,
745 'signs': signs,
746 'index_arrays': index_arrays,
747 'index_sizes': index_sizes,
748 'n_pts_with_derivs': n_pts_with_derivs,
749 'dd_flat_indices': dd_flat_indices,
750 'n_deriv_types': n_deriv_types,
751 # Fused kernel data
752 'idx_flat': idx_flat,
753 'idx_offsets': idx_offsets,
754 'row_offsets': row_offsets,
755 'col_offsets': col_offsets,
756 'fd_flat_indices': fd_flat_indices,
757 'df_flat_indices': df_flat_indices,
758 }
761def rbf_kernel_fast(phi_exp_3d, plan, out=None):
762 """
763 Fast kernel assembly using a precomputed plan and fused numba kernel.
765 Parameters
766 ----------
767 phi_exp_3d : ndarray of shape (n_derivs, n_rows_func, n_cols_func)
768 Pre-reshaped expanded derivative array.
769 plan : dict
770 Precomputed plan from precompute_kernel_plan().
771 out : ndarray, optional
772 Pre-allocated output array of shape (total, total). If None, a new
773 array is allocated. Reusing a buffer avoids repeated allocation of
774 large matrices during optimization loops.
776 Returns
777 -------
778 K : ndarray
779 Full kernel matrix.
780 """
781 n_rows_func = phi_exp_3d.shape[1]
782 n_cols_func = phi_exp_3d.shape[2]
783 total = n_rows_func + plan['n_pts_with_derivs']
784 if out is not None:
785 K = out
786 else:
787 K = np.empty((total, total))
789 # Use cached offsets if available, otherwise compute them
790 if 'row_offsets_abs' in plan:
791 row_off = plan['row_offsets_abs']
792 col_off = plan['col_offsets_abs']
793 else:
794 row_off = plan['row_offsets'] + n_rows_func
795 col_off = plan['col_offsets'] + n_cols_func
797 _assemble_kernel_numba(
798 phi_exp_3d, K, n_rows_func, n_cols_func,
799 plan['fd_flat_indices'], plan['df_flat_indices'], plan['dd_flat_indices'],
800 plan['idx_flat'], plan['idx_offsets'], plan['index_sizes'],
801 plan['signs'], plan['n_deriv_types'], row_off, col_off,
802 )
804 return K
807def rbf_kernel_predictions(
808 phi,
809 phi_exp,
810 n_order,
811 n_bases,
812 der_indices,
813 powers,
814 return_deriv,
815 index=None,
816 common_derivs=None,
817 calc_cov=False,
818 powers_predict=None
819):
820 """
821 Constructs the RBF kernel matrix for predictions with derivative entries.
823 This version uses Numba-accelerated functions for efficient matrix slicing.
825 Parameters
826 ----------
827 phi : OTI array
828 Base kernel matrix between test and training points.
829 phi_exp : ndarray
830 Expanded derivative array from phi.get_all_derivs().
831 n_order : int
832 Maximum derivative order.
833 n_bases : int
834 Number of input dimensions.
835 der_indices : list
836 Derivative specifications for training data.
837 powers : list of int
838 Sign powers for each derivative type.
839 return_deriv : bool
840 If True, predict derivatives at ALL test points.
841 index : list of lists or None
842 Training point indices for each derivative type.
843 common_derivs : list
844 Common derivative indices to predict.
845 calc_cov : bool
846 If True, computing covariance (use all indices for rows).
847 powers_predict : list of int, optional
848 Sign powers for prediction derivatives.
850 Returns
851 -------
852 K : ndarray
853 Prediction kernel matrix.
854 """
855 # Early return for covariance-only case
856 if calc_cov and not return_deriv:
857 return phi.real
859 dh = coti.get_dHelp()
861 n_rows_func, n_cols_func = phi.shape
862 n_deriv_types = len(der_indices)
863 n_deriv_types_pred = len(common_derivs) if common_derivs else 0
865 # Pre-compute signs
866 signs = np.array([(-1.0) ** p for p in powers], dtype=np.float64)
867 if powers_predict is not None:
868 signs_predict = np.array(
869 [(-1.0) ** p for p in powers_predict], dtype=np.float64)
870 else:
871 signs_predict = signs
873 # Determine derivative map and index structures
874 if return_deriv:
875 der_map = deriv_map(n_bases, 2 * n_order)
876 index_2 = np.arange(n_cols_func, dtype=np.int64)
877 if calc_cov:
878 index_cov = np.arange(n_cols_func, dtype=np.int64)
879 n_deriv_types = n_deriv_types_pred
880 n_pts_with_derivs_rows = n_deriv_types * n_cols_func
881 else:
882 n_pts_with_derivs_rows = sum(len(order_indices)
883 for order_indices in index) if index else 0
884 else:
885 der_map = deriv_map(n_bases, n_order)
886 index_2 = np.array([], dtype=np.int64)
887 n_pts_with_derivs_rows = sum(len(order_indices)
888 for order_indices in index) if index else 0
890 der_indices_tr, der_ind_order = transform_der_indices(der_indices, der_map)
891 der_indices_tr_pred, der_ind_order_pred = transform_der_indices(
892 common_derivs, der_map) if common_derivs else ([], [])
893 n_pts_with_derivs_cols = n_deriv_types_pred * len(index_2)
895 total_rows = n_rows_func + n_pts_with_derivs_rows
896 total_cols = n_cols_func + n_pts_with_derivs_cols
898 K = np.zeros((total_rows, total_cols))
899 base_shape = (n_rows_func, n_cols_func)
901 # Convert index lists to numpy arrays for numba
902 if index is not None and len(index) > 0 and isinstance(index[0], (list, np.ndarray)):
903 index_arrays = [np.asarray(idx, dtype=np.int64) for idx in index]
904 else:
905 index_arrays = []
907 # Block (0,0): Function-Function (K_ff)
908 content_full = phi_exp[0].reshape(base_shape)
909 K[:n_rows_func, :n_cols_func] = content_full * signs[0]
911 if not return_deriv:
912 # First Block-Column: Derivative-Function (K_df)
913 row_offset = n_rows_func
914 for i in range(n_deriv_types):
915 if not index_arrays:
916 break
918 row_indices = index_arrays[i]
919 n_pts_row = len(row_indices)
921 flat_idx = der_indices_tr[i]
922 content_full = phi_exp[flat_idx].reshape(base_shape)
924 # Use numba for efficient row extraction
925 extract_rows_and_assign(content_full, row_indices, K,
926 row_offset, 0, n_cols_func, signs[0])
927 row_offset += n_pts_row
928 return K
930 # --- return_deriv=True case ---
932 # First Block-Row: Function-Derivative (K_fd)
933 col_offset = n_cols_func
934 for j in range(n_deriv_types_pred):
935 n_pts_col = len(index_2)
937 flat_idx = der_indices_tr_pred[j]
938 content_full = phi_exp[flat_idx].reshape(base_shape)
940 # Use numba for efficient column extraction
941 extract_cols_and_assign(content_full, index_2, K,
942 0, col_offset, n_rows_func, signs_predict[j + 1])
943 col_offset += n_pts_col
945 # First Block-Column: Derivative-Function (K_df)
946 row_offset = n_rows_func
947 for i in range(n_deriv_types):
948 if calc_cov:
949 row_indices = index_cov
950 flat_idx = der_indices_tr_pred[i]
951 else:
952 if not index_arrays:
953 break
954 row_indices = index_arrays[i]
955 flat_idx = der_indices_tr[i]
956 n_pts_row = len(row_indices)
958 content_full = phi_exp[flat_idx].reshape(base_shape)
960 # Use numba for efficient row extraction
961 extract_rows_and_assign(content_full, row_indices, K,
962 row_offset, 0, n_cols_func, signs[0])
963 row_offset += n_pts_row
965 # Inner Blocks: Derivative-Derivative (K_dd)
966 row_offset = n_rows_func
967 for i in range(n_deriv_types):
968 if calc_cov:
969 row_indices = index_cov
970 else:
971 if not index_arrays:
972 break
973 row_indices = index_arrays[i]
974 n_pts_row = len(row_indices)
976 col_offset = n_cols_func
977 for j in range(n_deriv_types_pred):
978 n_pts_col = len(index_2)
980 imdir1 = der_ind_order_pred[j]
981 imdir2 = der_ind_order_pred[i] if calc_cov else der_ind_order[i]
982 new_idx, new_ord = dh.mult_dir(
983 imdir1[0], imdir1[1], imdir2[0], imdir2[1])
984 flat_idx = der_map[new_ord][new_idx]
986 content_full = phi_exp[flat_idx].reshape(base_shape)
988 # Use numba for efficient submatrix extraction and assignment (replaces np.ix_)
989 extract_and_assign(content_full, row_indices, index_2, K,
990 row_offset, col_offset, signs_predict[j + 1])
991 col_offset += n_pts_col
992 row_offset += n_pts_row
994 return K