Coverage for jetgp/wdegp/wdegp_utils.py: 58%

451 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-04-10 23:19 -0500

1import pyoti.core as coti 

2import numpy as np 

3from line_profiler import profile 

4import numba 

5 

6 

7# ============================================================================= 

8# Numba-accelerated helper functions for efficient matrix slicing 

9# ============================================================================= 

10 

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. 

15  

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. 

24  

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 

37 

38 

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. 

43  

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. 

52  

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 

64 

65 

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. 

71  

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. 

80  

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 

94 

95 

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. 

102  

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 

126 

127 

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. 

133  

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 

156 

157 

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. 

163  

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 

185 

186def differences_by_dim_func(X1, X2, n_order, oti_module, return_deriv=True): 

187 """ 

188 Compute pairwise differences between two input arrays X1 and X2 for each dimension, 

189 embedding hypercomplex units along each dimension for automatic differentiation. 

190 

191 For each dimension k, this function computes: 

192 diff_k[i, j] = X1[i, k] + e_{k+1} - X2[j, k] 

193 where e_{k+1} is a hypercomplex unit for the (k+1)-th dimension with order 2 * n_order. 

194 

195 Parameters 

196 ---------- 

197 X1 : array_like of shape (n1, d) 

198 First set of input points with n1 samples in d dimensions. 

199 X2 : array_like of shape (n2, d) 

200 Second set of input points with n2 samples in d dimensions. 

201 n_order : int 

202 The base order used to construct hypercomplex units (e_{k+1}) with order 2 * n_order. 

203 oti_module : module 

204 The PyOTI static module (e.g., pyoti.static.onumm4n2). 

205 return_deriv : bool, optional 

206 If True, use 2*n_order for derivative predictions. 

207 

208 Returns 

209 ------- 

210 differences_by_dim : list of length d 

211 A list where each element is an array of shape (n1, n2), containing the differences 

212 between corresponding dimensions of X1 and X2, augmented with hypercomplex units. 

213 """ 

214 # Keep numpy copies for fused path 

215 X1_np = np.asarray(X1, dtype=np.float64) 

216 X2_np = np.asarray(X2, dtype=np.float64) 

217 n1, d = X1_np.shape 

218 n2 = X2_np.shape[0] 

219 

220 # Check if the fused C-level function is available 

221 _use_fused = hasattr(oti_module.zeros((1, 1)), 'fused_from_real_with_perturbations') 

222 

223 if _use_fused: 

224 # --- Fused path: numpy broadcast for real part, C-level OTI fill --- 

225 differences_by_dim = [] 

226 perturb2 = oti_module.zeros((n2, 1)) # X2 has no perturbation in WDEGP 

227 

228 if n_order == 0: 

229 # No perturbation needed — just real differences 

230 perturb1 = oti_module.zeros((n1, 1)) 

231 for k in range(d): 

232 real_diffs = np.ascontiguousarray( 

233 X1_np[:, k:k+1] - X2_np[:, k:k+1].T, dtype=np.float64 

234 ) 

235 diffs_k = oti_module.empty((n1, n2)) 

236 diffs_k.fused_from_real_with_perturbations(real_diffs, perturb1, perturb2) 

237 differences_by_dim.append(diffs_k) 

238 else: 

239 if return_deriv: 

240 hc_order = 2 * n_order 

241 else: 

242 hc_order = n_order 

243 

244 for k in range(d): 

245 # Real differences via numpy broadcasting (fast) 

246 real_diffs = np.ascontiguousarray( 

247 X1_np[:, k:k+1] - X2_np[:, k:k+1].T, dtype=np.float64 

248 ) 

249 # Perturbation: e_{k+1} broadcast to all n1 points 

250 perturb1 = oti_module.zeros((n1, 1)) + oti_module.e(k + 1, order=hc_order) 

251 # Fused fill: out[i,j].real = real_diffs[i,j], out[i,j].im = perturb1[i] - 0 

252 diffs_k = oti_module.empty((n1, n2)) 

253 diffs_k.fused_from_real_with_perturbations(real_diffs, perturb1, perturb2) 

254 differences_by_dim.append(diffs_k) 

255 

256 return differences_by_dim 

257 

258 # --- Fallback: original Python loop path --- 

259 X1 = oti_module.array(X1_np) 

260 X2 = oti_module.array(X2_np) 

261 

262 differences_by_dim = [] 

263 

264 if n_order == 0: 

265 for k in range(d): 

266 diffs_k = oti_module.zeros((n1, n2)) 

267 for i in range(n1): 

268 diffs_k[i, :] = X1[i, k] - (oti_module.transpose(X2[:, k])) 

269 differences_by_dim.append(diffs_k) 

270 elif not return_deriv: 

271 for k in range(d): 

272 diffs_k = oti_module.zeros((n1, n2)) 

273 for i in range(n1): 

274 diffs_k[i, :] = ( 

275 X1[i, k] 

276 + oti_module.e(k + 1, order=n_order) 

277 - (X2[:, k].T) 

278 ) 

279 differences_by_dim.append(diffs_k) 

280 else: 

281 for k in range(d): 

282 diffs_k = oti_module.zeros((n1, n2)) 

283 for i in range(n1): 

284 diffs_k[i, :] = X1[i, k] - (X2[:, k].T) 

285 differences_by_dim.append(diffs_k + oti_module.e(k + 1, order=2 * n_order)) 

286 

287 return differences_by_dim 

288 

289 

290# ============================================================================= 

291# Derivative mapping utilities 

292# ============================================================================= 

293 

294def deriv_map(nbases, order): 

295 """ 

296 Creates a mapping from (order, index_within_order) to a single 

297 flattened index for all derivative components. 

298 """ 

299 k = 0 

300 map_deriv = [] 

301 for ordi in range(order + 1): 

302 ndir = coti.ndir_order(nbases, ordi) 

303 map_deriv_i = [0] * ndir 

304 for idx in range(ndir): 

305 map_deriv_i[idx] = k 

306 k += 1 

307 map_deriv.append(map_deriv_i) 

308 return map_deriv 

309 

310 

311def transform_der_indices(der_indices, der_map): 

312 """ 

313 Transforms a list of user-facing derivative specifications into the 

314 internal (order, index) format and the final flattened index. 

315 """ 

316 deriv_ind_transf = [] 

317 deriv_ind_order = [] 

318 for deriv in der_indices: 

319 imdir = coti.imdir(deriv) 

320 idx, order = imdir 

321 deriv_ind_transf.append(der_map[order][idx]) 

322 deriv_ind_order.append(imdir) 

323 return deriv_ind_transf, deriv_ind_order 

324 

325 

326# ============================================================================= 

327# RBF Kernel Assembly Functions (Optimized with Numba) 

328# ============================================================================= 

329 

330def rbf_kernel( 

331 phi, 

332 phi_exp, 

333 n_order, 

334 n_bases, 

335 der_indices, 

336 powers, 

337 index=-1, 

338): 

339 """ 

340 Constructs the RBF kernel matrix with derivative entries using an 

341 efficient pre-allocation strategy combined with a single call to 

342 extract all derivative components. 

343  

344 This version uses Numba-accelerated functions for efficient matrix slicing, 

345 replacing expensive np.ix_ operations. 

346 

347 Parameters 

348 ---------- 

349 phi : OTI array 

350 Base kernel matrix from kernel_func(differences, length_scales). 

351 phi_exp : ndarray 

352 Expanded derivative array from phi.get_all_derivs(). 

353 n_order : int 

354 Maximum derivative order. 

355 n_bases : int 

356 Number of OTI bases. 

357 der_indices : list 

358 Derivative specifications. 

359 powers : list of int 

360 Sign powers for each derivative type. 

361 index : list of lists 

362 Training point indices for each derivative type. 

363 

364 Returns 

365 ------- 

366 K : ndarray 

367 Full RBF kernel matrix with mixed function and derivative entries. 

368 """ 

369 dh = coti.get_dHelp() 

370 

371 # Create maps to translate derivative specifications to flat indices 

372 der_map = deriv_map(n_bases, 2 * n_order) 

373 der_indices_tr, der_ind_order = transform_der_indices(der_indices, der_map) 

374 

375 # Determine Block Sizes and Pre-allocate Matrix 

376 n_rows_func, n_cols_func = phi.shape 

377 n_deriv_types = len(der_indices) 

378 n_pts_with_derivs_cols = sum(len(order_indices) for order_indices in index) 

379 n_pts_with_derivs_rows = sum(len(order_indices) for order_indices in index) 

380 total_rows = n_rows_func + n_pts_with_derivs_rows 

381 total_cols = n_cols_func + n_pts_with_derivs_cols 

382 

383 K = np.zeros((total_rows, total_cols)) 

384 base_shape = (n_rows_func, n_cols_func) 

385 

386 # Pre-compute signs (avoid repeated exponentiation) 

387 signs = np.array([(-1.0) ** p for p in powers], dtype=np.float64) 

388 

389 # Convert index lists to numpy arrays for numba 

390 index_arrays = [np.asarray(idx, dtype=np.int64) for idx in index] 

391 

392 # Block (0,0): Function-Function (K_ff) 

393 content_full = phi_exp[0].reshape(base_shape) 

394 K[:n_rows_func, :n_cols_func] = content_full * signs[0] 

395 

396 # First Block-Row: Function-Derivative (K_fd) 

397 col_offset = n_cols_func 

398 for j in range(n_deriv_types): 

399 flat_idx = der_indices_tr[j] 

400 content_full = phi_exp[flat_idx].reshape(base_shape) 

401 current_indices = index_arrays[j] 

402 n_pts_this_order = len(current_indices) 

403 

404 # Use numba for efficient column extraction and assignment 

405 extract_cols_and_assign(content_full, current_indices, K, 

406 0, col_offset, n_rows_func, signs[j + 1]) 

407 col_offset += n_pts_this_order 

408 

409 # First Block-Column: Derivative-Function (K_df) 

410 row_offset = n_rows_func 

411 for i in range(n_deriv_types): 

412 flat_idx = der_indices_tr[i] 

413 content_full = phi_exp[flat_idx].reshape(base_shape) 

414 current_indices = index_arrays[i] 

415 n_pts_this_order = len(current_indices) 

416 

417 # Use numba for efficient row extraction and assignment 

418 extract_rows_and_assign(content_full, current_indices, K, 

419 row_offset, 0, n_cols_func, signs[0]) 

420 row_offset += n_pts_this_order 

421 

422 # Inner Blocks: Derivative-Derivative (K_dd) 

423 row_offset = n_rows_func 

424 for i in range(n_deriv_types): 

425 col_offset = n_cols_func 

426 row_indices = index_arrays[i] 

427 n_pts_row = len(row_indices) 

428 

429 for j in range(n_deriv_types): 

430 col_indices = index_arrays[j] 

431 n_pts_col = len(col_indices) 

432 

433 # Multiply the derivative indices to find the correct flat index 

434 imdir1 = der_ind_order[j] 

435 imdir2 = der_ind_order[i] 

436 new_idx, new_ord = dh.mult_dir( 

437 imdir1[0], imdir1[1], imdir2[0], imdir2[1]) 

438 flat_idx = der_map[new_ord][new_idx] 

439 content_full = phi_exp[flat_idx].reshape(base_shape) 

440 

441 # Use numba for efficient submatrix extraction and assignment (replaces np.ix_) 

442 extract_and_assign(content_full, row_indices, col_indices, K, 

443 row_offset, col_offset, signs[j + 1]) 

444 

445 col_offset += n_pts_col 

446 row_offset += n_pts_row 

447 

448 return K 

449 

450 

451@numba.jit(nopython=True, cache=True) 

452def _assemble_kernel_numba(phi_exp_3d, K, n_rows_func, n_cols_func, 

453 fd_flat_indices, df_flat_indices, dd_flat_indices, 

454 idx_flat, idx_offsets, idx_sizes, 

455 signs, n_deriv_types, row_offsets, col_offsets): 

456 """ 

457 Fused numba kernel that assembles the entire K matrix in a single call. 

458 Handles ff, fd, df, and dd blocks without Python-level loop overhead. 

459 """ 

460 # Block (0,0): Function-Function 

461 s0 = signs[0] 

462 for r in range(n_rows_func): 

463 for c in range(n_cols_func): 

464 K[r, c] = phi_exp_3d[0, r, c] * s0 

465 

466 # First Block-Row: Function-Derivative (fd) 

467 for j in range(n_deriv_types): 

468 fi = fd_flat_indices[j] 

469 sj = signs[j + 1] 

470 co = col_offsets[j] 

471 off_j = idx_offsets[j] 

472 sz_j = idx_sizes[j] 

473 for r in range(n_rows_func): 

474 for k in range(sz_j): 

475 ci = idx_flat[off_j + k] 

476 K[r, co + k] = phi_exp_3d[fi, r, ci] * sj 

477 

478 # First Block-Column: Derivative-Function (df) 

479 for i in range(n_deriv_types): 

480 fi = df_flat_indices[i] 

481 ro = row_offsets[i] 

482 off_i = idx_offsets[i] 

483 sz_i = idx_sizes[i] 

484 for k in range(sz_i): 

485 ri = idx_flat[off_i + k] 

486 for c in range(n_cols_func): 

487 K[ro + k, c] = phi_exp_3d[fi, ri, c] * s0 

488 

489 # Inner Blocks: Derivative-Derivative (dd) 

490 for i in range(n_deriv_types): 

491 ro = row_offsets[i] 

492 off_i = idx_offsets[i] 

493 sz_i = idx_sizes[i] 

494 for j in range(n_deriv_types): 

495 fi = dd_flat_indices[i, j] 

496 sj = signs[j + 1] 

497 co = col_offsets[j] 

498 off_j = idx_offsets[j] 

499 sz_j = idx_sizes[j] 

500 for ki in range(sz_i): 

501 ri = idx_flat[off_i + ki] 

502 for kj in range(sz_j): 

503 ci = idx_flat[off_j + kj] 

504 K[ro + ki, co + kj] = phi_exp_3d[fi, ri, ci] * sj 

505 

506 

507@numba.jit(nopython=True, cache=True) 

508def _project_W_to_phi_space(W, W_proj, n_rows_func, n_cols_func, 

509 fd_flat_indices, df_flat_indices, dd_flat_indices, 

510 idx_flat, idx_offsets, idx_sizes, 

511 signs, n_deriv_types, row_offsets, col_offsets): 

512 """ 

513 Reverse of _assemble_kernel_numba: project W from K-space back into 

514 phi_exp-space so that vdot(W, assemble(dphi_exp)) == vdot(W_proj, dphi_exp). 

515 """ 

516 for d in range(W_proj.shape[0]): 

517 for r in range(W_proj.shape[1]): 

518 for c in range(W_proj.shape[2]): 

519 W_proj[d, r, c] = 0.0 

520 s0 = signs[0] 

521 for r in range(n_rows_func): 

522 for c in range(n_cols_func): 

523 W_proj[0, r, c] += s0 * W[r, c] 

524 for j in range(n_deriv_types): 

525 fi = fd_flat_indices[j] 

526 sj = signs[j + 1] 

527 co = col_offsets[j] 

528 off_j = idx_offsets[j] 

529 sz_j = idx_sizes[j] 

530 for r in range(n_rows_func): 

531 for k in range(sz_j): 

532 ci = idx_flat[off_j + k] 

533 W_proj[fi, r, ci] += sj * W[r, co + k] 

534 for i in range(n_deriv_types): 

535 fi = df_flat_indices[i] 

536 ro = row_offsets[i] 

537 off_i = idx_offsets[i] 

538 sz_i = idx_sizes[i] 

539 for k in range(sz_i): 

540 ri = idx_flat[off_i + k] 

541 for c in range(n_cols_func): 

542 W_proj[fi, ri, c] += s0 * W[ro + k, c] 

543 for i in range(n_deriv_types): 

544 ro = row_offsets[i] 

545 off_i = idx_offsets[i] 

546 sz_i = idx_sizes[i] 

547 for j in range(n_deriv_types): 

548 fi = dd_flat_indices[i, j] 

549 sj = signs[j + 1] 

550 co = col_offsets[j] 

551 off_j = idx_offsets[j] 

552 sz_j = idx_sizes[j] 

553 for ki in range(sz_i): 

554 ri = idx_flat[off_i + ki] 

555 for kj in range(sz_j): 

556 ci = idx_flat[off_j + kj] 

557 W_proj[fi, ri, ci] += sj * W[ro + ki, co + kj] 

558 

559 

560@numba.jit(nopython=True, cache=True) 

561def _project_W_to_phi_space_accum(W, W_proj, n_rows_func, n_cols_func, 

562 fd_flat_indices, df_flat_indices, dd_flat_indices, 

563 idx_flat, idx_offsets, idx_sizes, 

564 signs, n_deriv_types, row_offsets, col_offsets): 

565 """ 

566 Like _project_W_to_phi_space but accumulates into W_proj without zeroing. 

567 Caller must zero W_proj before the first call. 

568 """ 

569 s0 = signs[0] 

570 for r in range(n_rows_func): 

571 for c in range(n_cols_func): 

572 W_proj[0, r, c] += s0 * W[r, c] 

573 for j in range(n_deriv_types): 

574 fi = fd_flat_indices[j] 

575 sj = signs[j + 1] 

576 co = col_offsets[j] 

577 off_j = idx_offsets[j] 

578 sz_j = idx_sizes[j] 

579 for r in range(n_rows_func): 

580 for k in range(sz_j): 

581 ci = idx_flat[off_j + k] 

582 W_proj[fi, r, ci] += sj * W[r, co + k] 

583 for i in range(n_deriv_types): 

584 fi = df_flat_indices[i] 

585 ro = row_offsets[i] 

586 off_i = idx_offsets[i] 

587 sz_i = idx_sizes[i] 

588 for k in range(sz_i): 

589 ri = idx_flat[off_i + k] 

590 for c in range(n_cols_func): 

591 W_proj[fi, ri, c] += s0 * W[ro + k, c] 

592 for i in range(n_deriv_types): 

593 ro = row_offsets[i] 

594 off_i = idx_offsets[i] 

595 sz_i = idx_sizes[i] 

596 for j in range(n_deriv_types): 

597 fi = dd_flat_indices[i, j] 

598 sj = signs[j + 1] 

599 co = col_offsets[j] 

600 off_j = idx_offsets[j] 

601 sz_j = idx_sizes[j] 

602 for ki in range(sz_i): 

603 ri = idx_flat[off_i + ki] 

604 for kj in range(sz_j): 

605 ci = idx_flat[off_j + kj] 

606 W_proj[fi, ri, ci] += sj * W[ro + ki, co + kj] 

607 

608 

609def precompute_kernel_plan(n_order, n_bases, der_indices, powers, index): 

610 """ 

611 Precompute all structural information needed by rbf_kernel so it can be 

612 reused across repeated calls with different phi_exp values. 

613 

614 Returns a dict containing flat indices, signs, index arrays, precomputed 

615 offsets/sizes, and mult_dir results for the dd block. 

616 """ 

617 dh = coti.get_dHelp() 

618 der_map = deriv_map(n_bases, 2 * n_order) 

619 der_indices_tr, der_ind_order = transform_der_indices(der_indices, der_map) 

620 

621 n_deriv_types = len(der_indices) 

622 signs = np.array([(-1.0) ** p for p in powers], dtype=np.float64) 

623 index_arrays = [np.asarray(idx, dtype=np.int64) for idx in index] 

624 

625 # Precompute sizes and offsets 

626 index_sizes = np.array([len(idx) for idx in index_arrays], dtype=np.int64) 

627 n_pts_with_derivs = int(index_sizes.sum()) 

628 

629 # Pack all index arrays into a single flat array with offsets 

630 idx_flat = np.concatenate(index_arrays) if n_deriv_types > 0 else np.array([], dtype=np.int64) 

631 idx_offsets = np.zeros(n_deriv_types, dtype=np.int64) 

632 for i in range(1, n_deriv_types): 

633 idx_offsets[i] = idx_offsets[i - 1] + index_sizes[i - 1] 

634 

635 # Precompute row/col offsets in K for each deriv type 

636 row_offsets = np.zeros(n_deriv_types, dtype=np.int64) 

637 col_offsets = np.zeros(n_deriv_types, dtype=np.int64) 

638 # Note: n_rows_func == n_cols_func for training kernel, but we store 

639 # offsets relative to n_rows_func which is added at call time 

640 cumsum = 0 

641 for i in range(n_deriv_types): 

642 row_offsets[i] = cumsum # relative to n_rows_func 

643 col_offsets[i] = cumsum # relative to n_cols_func 

644 cumsum += index_sizes[i] 

645 

646 # Precompute mult_dir results for dd blocks 

647 dd_flat_indices = np.empty((n_deriv_types, n_deriv_types), dtype=np.int64) 

648 for i in range(n_deriv_types): 

649 for j in range(n_deriv_types): 

650 imdir1 = der_ind_order[j] 

651 imdir2 = der_ind_order[i] 

652 new_idx, new_ord = dh.mult_dir( 

653 imdir1[0], imdir1[1], imdir2[0], imdir2[1]) 

654 dd_flat_indices[i, j] = der_map[new_ord][new_idx] 

655 

656 # fd and df flat indices as arrays 

657 fd_flat_indices = np.array(der_indices_tr, dtype=np.int64) 

658 df_flat_indices = np.array(der_indices_tr, dtype=np.int64) 

659 

660 return { 

661 'der_indices_tr': der_indices_tr, 

662 'signs': signs, 

663 'index_arrays': index_arrays, 

664 'index_sizes': index_sizes, 

665 'n_pts_with_derivs': n_pts_with_derivs, 

666 'dd_flat_indices': dd_flat_indices, 

667 'n_deriv_types': n_deriv_types, 

668 # Fused kernel data 

669 'idx_flat': idx_flat, 

670 'idx_offsets': idx_offsets, 

671 'row_offsets': row_offsets, 

672 'col_offsets': col_offsets, 

673 'fd_flat_indices': fd_flat_indices, 

674 'df_flat_indices': df_flat_indices, 

675 } 

676 

677 

678def rbf_kernel_fast(phi_exp_3d, plan, out=None): 

679 """ 

680 Fast kernel assembly using a precomputed plan and fused numba kernel. 

681 

682 Parameters 

683 ---------- 

684 phi_exp_3d : ndarray of shape (n_derivs, n_rows_func, n_cols_func) 

685 Pre-reshaped expanded derivative array. 

686 plan : dict 

687 Precomputed plan from precompute_kernel_plan(). 

688 out : ndarray, optional 

689 Pre-allocated output array. If None, a new array is allocated. 

690 

691 Returns 

692 ------- 

693 K : ndarray 

694 Full kernel matrix. 

695 """ 

696 n_rows_func = phi_exp_3d.shape[1] 

697 n_cols_func = phi_exp_3d.shape[2] 

698 total = n_rows_func + plan['n_pts_with_derivs'] 

699 if out is not None: 

700 K = out 

701 else: 

702 K = np.empty((total, total)) 

703 

704 if 'row_offsets_abs' in plan: 

705 row_off = plan['row_offsets_abs'] 

706 col_off = plan['col_offsets_abs'] 

707 else: 

708 row_off = plan['row_offsets'] + n_rows_func 

709 col_off = plan['col_offsets'] + n_cols_func 

710 

711 _assemble_kernel_numba( 

712 phi_exp_3d, K, n_rows_func, n_cols_func, 

713 plan['fd_flat_indices'], plan['df_flat_indices'], plan['dd_flat_indices'], 

714 plan['idx_flat'], plan['idx_offsets'], plan['index_sizes'], 

715 plan['signs'], plan['n_deriv_types'], row_off, col_off, 

716 ) 

717 

718 return K 

719 

720 

721def rbf_kernel_predictions( 

722 phi, 

723 phi_exp, 

724 n_order, 

725 n_bases, 

726 der_indices, 

727 powers, 

728 return_deriv, 

729 index=-1, 

730 common_derivs=None, 

731 calc_cov=False, 

732 powers_predict=None 

733): 

734 """ 

735 Constructs the RBF kernel matrix for predictions with derivative entries. 

736  

737 This version uses Numba-accelerated functions for efficient matrix slicing. 

738 

739 Parameters 

740 ---------- 

741 phi : OTI array 

742 Base kernel matrix between test and training points. 

743 phi_exp : ndarray 

744 Expanded derivative array from phi.get_all_derivs(). 

745 n_order : int 

746 Maximum derivative order. 

747 n_bases : int 

748 Number of OTI bases. 

749 der_indices : list 

750 Derivative specifications for training data. 

751 powers : list of int 

752 Sign powers for each derivative type. 

753 return_deriv : bool 

754 If True, predict derivatives at test points. 

755 index : list of lists 

756 Training point indices for each derivative type. 

757 common_derivs : list 

758 Common derivative indices to predict. 

759 calc_cov : bool 

760 If True, computing covariance. 

761 powers_predict : list of int, optional 

762 Sign powers for prediction derivatives. 

763 

764 Returns 

765 ------- 

766 K : ndarray 

767 Prediction kernel matrix. 

768 """ 

769 if calc_cov and not return_deriv: 

770 return phi.real 

771 

772 dh = coti.get_dHelp() 

773 

774 n_rows_func, n_cols_func = phi.shape 

775 n_deriv_types = len(der_indices) 

776 n_deriv_types_pred = len(common_derivs) if common_derivs else 0 

777 

778 # Pre-compute signs 

779 signs = np.array([(-1.0) ** p for p in powers], dtype=np.float64) 

780 if powers_predict is not None: 

781 signs_predict = np.array([(-1.0) ** p for p in powers_predict], dtype=np.float64) 

782 else: 

783 signs_predict = signs 

784 

785 if return_deriv: 

786 der_map = deriv_map(n_bases, 2 * n_order) 

787 index_2 = np.arange(phi_exp.shape[-1], dtype=np.int64) 

788 if calc_cov: 

789 index_cov = np.arange(phi_exp.shape[-1], dtype=np.int64) 

790 n_deriv_types = n_deriv_types_pred 

791 n_pts_with_derivs_rows = n_deriv_types * len([i for i in range(n_cols_func) if i < len(index_2)]) 

792 else: 

793 n_pts_with_derivs_rows = sum(len(order_indices) for order_indices in index) if isinstance(index, list) else 0 

794 else: 

795 der_map = deriv_map(n_bases, n_order) 

796 index_2 = np.array([], dtype=np.int64) 

797 n_pts_with_derivs_rows = sum(len(order_indices) for order_indices in index) if isinstance(index, list) else 0 

798 

799 der_indices_tr, der_ind_order = transform_der_indices(der_indices, der_map) 

800 der_indices_tr_pred, der_ind_order_pred = transform_der_indices(common_derivs, der_map) if common_derivs else ([], []) 

801 n_pts_with_derivs_cols = n_deriv_types_pred * len([i for i in range(n_cols_func) if i < len(index_2)]) 

802 

803 total_rows = n_rows_func + n_pts_with_derivs_rows 

804 total_cols = n_cols_func + n_pts_with_derivs_cols 

805 

806 K = np.zeros((total_rows, total_cols)) 

807 base_shape = (n_rows_func, n_cols_func) 

808 

809 # Convert index lists to numpy arrays for numba 

810 if isinstance(index, list) and len(index) > 0: 

811 index_arrays = [np.asarray(idx, dtype=np.int64) for idx in index] 

812 else: 

813 index_arrays = [] 

814 

815 # Block (0,0): Function-Function (K_ff) 

816 content_full = phi_exp[0].reshape(base_shape) 

817 K[:n_rows_func, :n_cols_func] = content_full * signs[0] 

818 

819 if not return_deriv: 

820 # First Block-Column: Derivative-Function (K_df) 

821 row_offset = n_rows_func 

822 for i in range(n_deriv_types): 

823 if not index_arrays: 

824 break 

825 row_indices = index_arrays[i] 

826 n_pts_row = len(row_indices) 

827 

828 flat_idx = der_indices_tr[i] 

829 content_full = phi_exp[flat_idx].reshape(base_shape) 

830 

831 # Use numba for efficient row extraction 

832 extract_rows_and_assign(content_full, row_indices, K, 

833 row_offset, 0, n_cols_func, signs[0]) 

834 row_offset += n_pts_row 

835 return K 

836 

837 # --- return_deriv=True case --- 

838 

839 # First Block-Row: Function-Derivative (K_fd) 

840 col_offset = n_cols_func 

841 for j in range(n_deriv_types_pred): 

842 col_indices = index_2 

843 n_pts_col = len(col_indices) 

844 

845 flat_idx = der_indices_tr_pred[j] 

846 content_full = phi_exp[flat_idx].reshape(base_shape) 

847 

848 # Use numba for efficient column extraction 

849 extract_cols_and_assign(content_full, col_indices, K, 

850 0, col_offset, n_rows_func, signs_predict[j + 1]) 

851 col_offset += n_pts_col 

852 

853 # First Block-Column: Derivative-Function (K_df) 

854 row_offset = n_rows_func 

855 for i in range(n_deriv_types): 

856 if calc_cov: 

857 row_indices = index_cov 

858 flat_idx = der_indices_tr_pred[i] 

859 else: 

860 if not index_arrays: 

861 break 

862 row_indices = index_arrays[i] 

863 flat_idx = der_indices_tr[i] 

864 n_pts_row = len(row_indices) 

865 

866 content_full = phi_exp[flat_idx].reshape(base_shape) 

867 

868 # Use numba for efficient row extraction 

869 extract_rows_and_assign(content_full, row_indices, K, 

870 row_offset, 0, n_cols_func, signs[0]) 

871 row_offset += n_pts_row 

872 

873 # Inner Blocks: Derivative-Derivative (K_dd) 

874 row_offset = n_rows_func 

875 for i in range(n_deriv_types): 

876 if calc_cov: 

877 row_indices = index_cov 

878 else: 

879 if not index_arrays: 

880 break 

881 row_indices = index_arrays[i] 

882 n_pts_row = len(row_indices) 

883 

884 col_offset = n_cols_func 

885 for j in range(n_deriv_types_pred): 

886 col_indices = index_2 

887 n_pts_col = len(col_indices) 

888 

889 # Multiply the derivative indices to find the correct flat index 

890 imdir1 = der_ind_order_pred[j] 

891 imdir2 = der_ind_order_pred[i] if calc_cov else der_ind_order[i] 

892 new_idx, new_ord = dh.mult_dir( 

893 imdir1[0], imdir1[1], imdir2[0], imdir2[1]) 

894 flat_idx = der_map[new_ord][new_idx] 

895 

896 content_full = phi_exp[flat_idx].reshape(base_shape) 

897 

898 # Use numba for efficient submatrix extraction and assignment (replaces np.ix_) 

899 extract_and_assign(content_full, row_indices, col_indices, K, 

900 row_offset, col_offset, signs_predict[j + 1]) 

901 col_offset += n_pts_col 

902 row_offset += n_pts_row 

903 

904 return K 

905 

906 

907# ============================================================================= 

908# Utility functions 

909# ============================================================================= 

910 

911def determine_weights(diffs_by_dim, diffs_test, length_scales, kernel_func, sigma_n): 

912 """ 

913 Vectorized version: compute interpolation weights for multiple test points at once. 

914  

915 Parameters 

916 ---------- 

917 diffs_by_dim : list of ndarray 

918 Pairwise differences between training points (by dimension). 

919 diffs_test : list of ndarray 

920 Pairwise differences between test points and training points (by dimension). 

921 Shape: each array is (n_test, n_train) or similar batch dimension. 

922 length_scales : array-like 

923 Kernel hyperparameters. 

924 kernel_func : callable 

925 Kernel function. 

926 sigma_n : float 

927 Noise parameter (if needed). 

928  

929 Returns 

930 ------- 

931 weights_matrix : ndarray of shape (n_test, n_train) 

932 Interpolation weights for each test point. 

933 """ 

934 # Compute K matrix (training covariance) - same for all test points 

935 K = kernel_func(diffs_by_dim, length_scales).real 

936 n_train = K.shape[0] 

937 

938 # Compute r vectors (test-train covariances) for all test points at once 

939 r_all = kernel_func(diffs_test, length_scales).real 

940 n_test = r_all.shape[0] 

941 

942 # Build augmented system matrix M (same for all test points) 

943 M = np.zeros((n_train + 1, n_train + 1)) 

944 M[:n_train, :n_train] = K 

945 M[:n_train, n_train] = 1 

946 M[n_train, :n_train] = 1 

947 M[n_train, n_train] = 0 

948 

949 # Build augmented RHS for all test points 

950 r_augmented = np.zeros((n_test, n_train + 1)) 

951 r_augmented[:, :n_train] = r_all 

952 r_augmented[:, n_train] = 1 

953 

954 # Solve for all test points at once 

955 solution = np.linalg.solve(M, r_augmented.T) 

956 

957 # Extract weights (exclude Lagrange multiplier) 

958 weights_matrix = solution[:n_train, :].T 

959 

960 return weights_matrix 

961 

962 

963def to_tuple(item): 

964 """Convert list to tuple recursively.""" 

965 if isinstance(item, list): 

966 return tuple(to_tuple(x) for x in item) 

967 return item 

968 

969 

970def to_list(x): 

971 """Convert tuple to list recursively.""" 

972 if isinstance(x, tuple): 

973 return [to_list(i) for i in x] 

974 return x 

975 

976 

977def find_common_derivatives(all_indices): 

978 """Find derivative indices common to all submodels.""" 

979 sets = [set(to_tuple(elem) for elem in idx_list) for idx_list in all_indices] 

980 return sets[0].intersection(*sets[1:])