Coverage for jetgp/full_gddegp/gddegp_utils.py: 65%

457 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-04-07 00:57 -0500

1import numpy as np 

2import pyoti.core as coti 

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) 

97def extract_submatrix_transposed(content_full, row_indices, col_indices): 

98 """ 

99 Extract submatrix and return its transpose. 

100 Replaces content_full[np.ix_(row_indices, col_indices)].T 

101  

102 Parameters 

103 ---------- 

104 content_full : ndarray of shape (n_rows_full, n_cols_full) 

105 Source matrix. 

106 row_indices : ndarray of int64 

107 Row indices to extract. 

108 col_indices : ndarray of int64 

109 Column indices to extract. 

110  

111 Returns 

112 ------- 

113 result : ndarray of shape (len(col_indices), len(row_indices)) 

114 Transposed extracted submatrix. 

115 """ 

116 n_rows = len(row_indices) 

117 n_cols = len(col_indices) 

118 result = np.empty((n_cols, n_rows)) 

119 for i in range(n_rows): 

120 ri = row_indices[i] 

121 for j in range(n_cols): 

122 result[j, i] = content_full[ri, col_indices[j]] 

123 return result 

124 

125 

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

127def extract_rows_transposed(content_full, row_indices, n_cols): 

128 """ 

129 Extract rows and return transposed result. 

130 Replaces content_full[row_indices, :].T 

131  

132 Parameters 

133 ---------- 

134 content_full : ndarray of shape (n_rows_full, n_cols) 

135 Source matrix. 

136 row_indices : ndarray of int64 

137 Row indices to extract. 

138 n_cols : int 

139 Number of columns. 

140  

141 Returns 

142 ------- 

143 result : ndarray of shape (n_cols, len(row_indices)) 

144 Transposed extracted rows. 

145 """ 

146 n_rows = len(row_indices) 

147 result = np.empty((n_cols, n_rows)) 

148 for i in range(n_rows): 

149 ri = row_indices[i] 

150 for j in range(n_cols): 

151 result[j, i] = content_full[ri, j] 

152 return result 

153 

154 

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

156def extract_cols_transposed(content_full, col_indices, n_rows): 

157 """ 

158 Extract columns and return transposed result. 

159 Replaces content_full[:, col_indices].T 

160  

161 Parameters 

162 ---------- 

163 content_full : ndarray of shape (n_rows, n_cols_full) 

164 Source matrix. 

165 col_indices : ndarray of int64 

166 Column indices to extract. 

167 n_rows : int 

168 Number of rows. 

169  

170 Returns 

171 ------- 

172 result : ndarray of shape (len(col_indices), n_rows) 

173 Transposed extracted columns. 

174 """ 

175 n_cols = len(col_indices) 

176 result = np.empty((n_cols, n_rows)) 

177 for i in range(n_rows): 

178 for j in range(n_cols): 

179 result[j, i] = content_full[i, col_indices[j]] 

180 return result 

181 

182 

183@numba.jit(nopython=True, cache=True, parallel=False) 

184def extract_and_assign(content_full, row_indices, col_indices, K, 

185 row_start, col_start): 

186 """ 

187 Extract submatrix and assign directly to K. 

188  

189 Parameters 

190 ---------- 

191 content_full : ndarray of shape (n_rows_full, n_cols_full) 

192 Source matrix. 

193 row_indices : ndarray of int64 

194 Row indices to extract. 

195 col_indices : ndarray of int64 

196 Column indices to extract. 

197 K : ndarray 

198 Target matrix to fill. 

199 row_start : int 

200 Starting row index in K. 

201 col_start : int 

202 Starting column index in K. 

203 """ 

204 n_rows = len(row_indices) 

205 n_cols = len(col_indices) 

206 for i in range(n_rows): 

207 ri = row_indices[i] 

208 for j in range(n_cols): 

209 K[row_start + i, col_start + j] = content_full[ri, col_indices[j]] 

210 

211 

212@numba.jit(nopython=True, cache=True, parallel=False) 

213def extract_and_assign_transposed(content_full, row_indices, col_indices, K, 

214 row_start, col_start): 

215 """ 

216 Extract submatrix and assign its transpose directly to K. 

217 Replaces K[...] = content_full[np.ix_(row_indices, col_indices)].T 

218  

219 Parameters 

220 ---------- 

221 content_full : ndarray of shape (n_rows_full, n_cols_full) 

222 Source matrix. 

223 row_indices : ndarray of int64 

224 Row indices to extract from content_full. 

225 col_indices : ndarray of int64 

226 Column indices to extract from content_full. 

227 K : ndarray 

228 Target matrix to fill. 

229 row_start : int 

230 Starting row index in K. 

231 col_start : int 

232 Starting column index in K. 

233 """ 

234 n_rows = len(row_indices) 

235 n_cols = len(col_indices) 

236 for i in range(n_rows): 

237 ri = row_indices[i] 

238 for j in range(n_cols): 

239 # Transposed assignment: K[col_idx, row_idx] = content[row_idx, col_idx] 

240 K[row_start + j, col_start + i] = content_full[ri, col_indices[j]] 

241 

242 

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

244def extract_rows_and_assign(content_full, row_indices, K, 

245 row_start, col_start, n_cols): 

246 """ 

247 Extract rows and assign directly to K. 

248  

249 Parameters 

250 ---------- 

251 content_full : ndarray of shape (n_rows_full, n_cols) 

252 Source matrix. 

253 row_indices : ndarray of int64 

254 Row indices to extract. 

255 K : ndarray 

256 Target matrix to fill. 

257 row_start : int 

258 Starting row index in K. 

259 col_start : int 

260 Starting column index in K. 

261 n_cols : int 

262 Number of columns to copy. 

263 """ 

264 n_rows = len(row_indices) 

265 for i in range(n_rows): 

266 ri = row_indices[i] 

267 for j in range(n_cols): 

268 K[row_start + i, col_start + j] = content_full[ri, j] 

269 

270 

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

272def extract_cols_and_assign(content_full, col_indices, K, 

273 row_start, col_start, n_rows): 

274 """ 

275 Extract columns and assign directly to K. 

276  

277 Parameters 

278 ---------- 

279 content_full : ndarray of shape (n_rows, n_cols_full) 

280 Source matrix. 

281 col_indices : ndarray of int64 

282 Column indices to extract. 

283 K : ndarray 

284 Target matrix to fill. 

285 row_start : int 

286 Starting row index in K. 

287 col_start : int 

288 Starting column index in K. 

289 n_rows : int 

290 Number of rows to copy. 

291 """ 

292 n_cols = len(col_indices) 

293 for i in range(n_rows): 

294 for j in range(n_cols): 

295 K[row_start + i, col_start + j] = content_full[i, col_indices[j]] 

296 

297 

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

299def extract_rows_and_assign_transposed(content_full, row_indices, K, 

300 row_start, col_start, n_cols): 

301 """ 

302 Extract rows and assign transposed result directly to K. 

303 Replaces K[...] = content_full[row_indices, :].T 

304  

305 Parameters 

306 ---------- 

307 content_full : ndarray of shape (n_rows_full, n_cols) 

308 Source matrix. 

309 row_indices : ndarray of int64 

310 Row indices to extract. 

311 K : ndarray 

312 Target matrix to fill. 

313 row_start : int 

314 Starting row index in K. 

315 col_start : int 

316 Starting column index in K. 

317 n_cols : int 

318 Number of columns in content_full. 

319 """ 

320 n_rows = len(row_indices) 

321 for i in range(n_rows): 

322 ri = row_indices[i] 

323 for j in range(n_cols): 

324 K[row_start + j, col_start + i] = content_full[ri, j] 

325 

326 

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

328def extract_cols_and_assign_transposed(content_full, col_indices, K, 

329 row_start, col_start, n_rows): 

330 """ 

331 Extract columns and assign transposed result directly to K. 

332 Replaces K[...] = content_full[:, col_indices].T 

333  

334 Parameters 

335 ---------- 

336 content_full : ndarray of shape (n_rows, n_cols_full) 

337 Source matrix. 

338 col_indices : ndarray of int64 

339 Column indices to extract. 

340 K : ndarray 

341 Target matrix to fill. 

342 row_start : int 

343 Starting row index in K. 

344 col_start : int 

345 Starting column index in K. 

346 n_rows : int 

347 Number of rows in content_full. 

348 """ 

349 n_cols = len(col_indices) 

350 for i in range(n_rows): 

351 for j in range(n_cols): 

352 K[row_start + j, col_start + i] = content_full[i, col_indices[j]] 

353 

354 

355# ============================================================================= 

356# Derivative index transformation utilities 

357# ============================================================================= 

358 

359def make_first_odd(der_indices): 

360 """Transform derivative indices to use odd bases (1, 3, 5, ...).""" 

361 result = [] 

362 for group in der_indices: 

363 new_group = [] 

364 for pair in group: 

365 first = pair[0] 

366 new_group.append([2 * first - 1, pair[1]]) 

367 result.append(new_group) 

368 return result 

369 

370 

371def make_first_even(der_indices): 

372 """Transform derivative indices to use even bases (2, 4, 6, ...).""" 

373 result = [] 

374 for group in der_indices: 

375 new_group = [] 

376 for pair in group: 

377 first = pair[0] 

378 new_group.append([2 * first, pair[1]]) 

379 result.append(new_group) 

380 return result 

381 

382 

383# ============================================================================= 

384# Difference computation functions 

385# ============================================================================= 

386def compute_dimension_differences(k, X1, X2, n1, n2, rays_X1, rays_X2, 

387 derivative_locations_X1, derivative_locations_X2, 

388 e_tags_1, e_tags_2, oti_module, 

389 X1_np=None, X2_np=None, use_fused=False): 

390 """ 

391 Compute differences for a single dimension k. 

392 Only perturbs points at specified derivative_locations with their corresponding rays. 

393 

394 Parameters 

395 ---------- 

396 k : int 

397 Dimension index. 

398 X1, X2 : oti.array or None 

399 Input point arrays of shape (n1, d) and (n2, d). Can be None if use_fused=True. 

400 n1, n2 : int 

401 Number of points in X1, X2. 

402 rays_X1 : list of ndarray or None 

403 rays_X1[i] has shape (d, len(derivative_locations_X1[i])). 

404 rays_X2 : list of ndarray or None 

405 rays_X2[i] has shape (d, len(derivative_locations_X2[i])). 

406 derivative_locations_X1 : list of list 

407 derivative_locations_X1[i] contains indices of X1 points with direction i. 

408 derivative_locations_X2 : list of list 

409 derivative_locations_X2[i] contains indices of X2 points with direction i. 

410 e_tags_1, e_tags_2 : list 

411 OTI basis elements for each direction. 

412 oti_module : module 

413 The PyOTI static module. 

414 X1_np, X2_np : ndarray or None 

415 Numpy arrays of shape (n1, d) and (n2, d). Required if use_fused=True. 

416 use_fused : bool 

417 If True, use fused_from_real_with_perturbations C-level function. 

418 

419 Returns 

420 ------- 

421 diffs_k : oti.array 

422 Differences for dimension k with shape (n1, n2). 

423 """ 

424 # Build perturbation vector for X1 

425 perturb_X1_values = [0.0] * n1 

426 if rays_X1 is not None: 

427 for dir_idx in range(len(rays_X1)): 

428 locs = derivative_locations_X1[dir_idx] 

429 rays = rays_X1[dir_idx] 

430 for j, pt_idx in enumerate(locs): 

431 perturb_X1_values[pt_idx] = perturb_X1_values[pt_idx] + e_tags_1[dir_idx] * rays[k, j] 

432 

433 # Build perturbation vector for X2 

434 perturb_X2_values = [0.0] * n2 

435 if rays_X2 is not None: 

436 for dir_idx in range(len(rays_X2)): 

437 locs = derivative_locations_X2[dir_idx] 

438 rays = rays_X2[dir_idx] 

439 for j, pt_idx in enumerate(locs): 

440 perturb_X2_values[pt_idx] = perturb_X2_values[pt_idx] + e_tags_2[dir_idx] * rays[k, j] 

441 

442 # Convert to OTI arrays 

443 perturb_X1 = oti_module.array(perturb_X1_values) 

444 perturb_X2 = oti_module.array(perturb_X2_values) 

445 

446 if use_fused: 

447 # --- Fused path --- 

448 real_diffs = np.ascontiguousarray( 

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

450 ) 

451 # Reshape perturbations from (n,) to (n, 1) for fused function 

452 perturb1 = oti_module.zeros((n1, 1)) + perturb_X1 

453 perturb2 = oti_module.zeros((n2, 1)) + perturb_X2 

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

455 diffs_k.fused_from_real_with_perturbations(real_diffs, perturb1, perturb2) 

456 return diffs_k 

457 

458 # --- Fallback path --- 

459 # Tag coordinates 

460 X1_k_tagged = X1[:, k] + perturb_X1 

461 X2_k_tagged = X2[:, k] + perturb_X2 

462 

463 # Compute differences 

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

465 for i in range(n1): 

466 diffs_k[i, :] = X1_k_tagged[i, 0] - oti_module.transpose(X2_k_tagged[:, 0]) 

467 

468 return diffs_k 

469 

470 

471def differences_by_dim_func(X1, X2, rays_X1, rays_X2, derivative_locations_X1, derivative_locations_X2, 

472 n_order, oti_module, return_deriv=True): 

473 """ 

474 Compute dimension-wise differences with OTI tagging on both X1 and X2. 

475 

476 GDDEGP uses a dual-tag OTI scheme: X1 points are tagged with odd bases 

477 (e_1, e_3, e_5, ...) and X2 points with even bases (e_2, e_4, e_6, ...). 

478 This requires ``n_bases = 2 * n_direction_types``. 

479 

480 The dual-tag approach is necessary because each point can have a unique 

481 directional ray, and the kernel matrix requires derivatives with respect to 

482 *both* sets of directions simultaneously. In the difference X1 - X2, the 

483 OTI coefficient for basis e_i at position (a, b) encodes only the ray of 

484 the point that was tagged with e_i. A single-tag scheme (tagging both X1 

485 and X2 with the same basis) would conflate the two rays in the difference, 

486 making it impossible to recover the correct cross-derivative 

487 ``v_i(a)^T H v_j(b)`` needed for K_dd blocks, and producing an asymmetric 

488 K_fd block when rays vary per point. 

489 

490 Parameters 

491 ---------- 

492 X1 : ndarray of shape (n1, d) 

493 First set of input points. 

494 X2 : ndarray of shape (n2, d) 

495 Second set of input points. 

496 rays_X1 : list of ndarray or None 

497 List of ray arrays for X1. rays_X1[i] has shape (d, len(derivative_locations_X1[i])). 

498 rays_X2 : list of ndarray or None 

499 List of ray arrays for X2. rays_X2[i] has shape (d, len(derivative_locations_X2[i])). 

500 derivative_locations_X1 : list of list 

501 derivative_locations_X1[i] contains indices of X1 points with derivative direction i. 

502 derivative_locations_X2 : list of list 

503 derivative_locations_X2[i] contains indices of X2 points with derivative direction i. 

504 n_order : int 

505 Derivative order for OTI tagging. 

506 oti_module : module 

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

508 return_deriv : bool, optional 

509 If True, use order 2*n_order for derivative-derivative blocks. 

510 

511 Returns 

512 ------- 

513 differences_by_dim : list of oti.array 

514 List of length d, each element is an (n1, n2) OTI array. 

515 """ 

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

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

518 n1, d = X1_np.shape 

519 n2 = X2_np.shape[0] 

520 

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

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

523 

524 # Determine number of derivative directions 

525 m1 = len(rays_X1) if rays_X1 is not None else 0 

526 m2 = len(rays_X2) if rays_X2 is not None else 0 

527 m = max(m1, m2) 

528 

529 # Pre-compute OTI basis elements 

530 e_tags_1 = [] 

531 e_tags_2 = [] 

532 

533 if n_order == 0: 

534 e_tags_1 = [0] * m 

535 e_tags_2 = [0] * m 

536 elif not return_deriv: 

537 for i in range(m): 

538 e_tags_1.append(oti_module.e((2 * i + 1), order=n_order)) 

539 e_tags_2.append(oti_module.e((2 * i + 2), order=n_order)) 

540 else: 

541 for i in range(m): 

542 e_tags_1.append(oti_module.e((2 * i + 1), order=2 * n_order)) 

543 e_tags_2.append(oti_module.e((2 * i + 2), order=2 * n_order)) 

544 

545 # Only convert to OTI arrays if using fallback path 

546 _use_fused_path = _use_fused and n_order > 0 

547 _use_fused_n0 = _use_fused and n_order == 0 

548 if _use_fused_path or _use_fused_n0: 

549 X1_oti = None 

550 X2_oti = None 

551 else: 

552 X1_oti = oti_module.array(X1_np) 

553 X2_oti = oti_module.array(X2_np) 

554 

555 # Compute differences for each dimension 

556 differences_by_dim = [] 

557 

558 if _use_fused_n0: 

559 # n_order == 0: no perturbation, just real differences via fused path 

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

561 perturb2 = oti_module.zeros((n2, 1)) 

562 for k in range(d): 

563 real_diffs = np.ascontiguousarray( 

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

565 ) 

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

567 diffs_k.fused_from_real_with_perturbations(real_diffs, perturb1, perturb2) 

568 differences_by_dim.append(diffs_k) 

569 return differences_by_dim 

570 

571 for k in range(d): 

572 diffs_k = compute_dimension_differences( 

573 k, X1_oti, X2_oti, n1, n2, rays_X1, rays_X2, 

574 derivative_locations_X1, derivative_locations_X2, 

575 e_tags_1, e_tags_2, oti_module, 

576 X1_np=X1_np, X2_np=X2_np, 

577 use_fused=_use_fused_path 

578 ) 

579 differences_by_dim.append(diffs_k) 

580 

581 return differences_by_dim 

582 

583 

584# ============================================================================= 

585# Derivative mapping utilities 

586# ============================================================================= 

587 

588def deriv_map(nbases, order): 

589 """Create mapping from (order, index) to flattened index.""" 

590 k = 0 

591 map_deriv = [] 

592 for ordi in range(order + 1): 

593 ndir = coti.ndir_order(nbases, ordi) 

594 map_deriv_i = [0] * ndir 

595 for idx in range(ndir): 

596 map_deriv_i[idx] = k 

597 k += 1 

598 map_deriv.append(map_deriv_i) 

599 return map_deriv 

600 

601 

602def transform_der_indices(der_indices, der_map): 

603 """Transform derivative indices to flattened format.""" 

604 deriv_ind_transf = [] 

605 deriv_ind_order = [] 

606 for deriv in der_indices: 

607 imdir = coti.imdir(deriv) 

608 idx, order = imdir 

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

610 deriv_ind_order.append(imdir) 

611 return deriv_ind_transf, deriv_ind_order 

612 

613 

614# ============================================================================= 

615# RBF Kernel Assembly Functions (Optimized with Numba) 

616# ============================================================================= 

617 

618@profile 

619def rbf_kernel( 

620 phi, 

621 phi_exp, 

622 n_order, 

623 n_bases, 

624 der_indices, 

625 index=None 

626): 

627 """ 

628 Assembles the full GDDEGP covariance matrix with selective derivative coverage. 

629  

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

631 replacing expensive np.ix_ operations. 

632 

633 Parameters 

634 ---------- 

635 phi : OTI array 

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

637 phi_exp : ndarray 

638 Expanded derivative array from phi.get_all_derivs(). 

639 n_order : int 

640 Maximum derivative order. 

641 n_bases : int 

642 Number of OTI bases (must be even). 

643 der_indices : list 

644 Derivative index specifications. 

645 index : list of list 

646 index[i] contains indices of points with derivative direction i. 

647 

648 Returns 

649 ------- 

650 K : ndarray 

651 Kernel matrix with block structure based on derivative locations. 

652 """ 

653 dh = coti.get_dHelp() 

654 

655 assert n_bases % 2 == 0, "n_bases must be an even number." 

656 PHIrows, PHIcols = phi.shape 

657 total_derivs = len(der_indices) 

658 

659 # Compute output matrix dimensions 

660 n_deriv_rows = sum(len(locs) for locs in index) 

661 n_deriv_cols = sum(len(locs) for locs in index) 

662 n_output_rows = PHIrows + n_deriv_rows 

663 n_output_cols = PHIcols + n_deriv_cols 

664 

665 der_map = deriv_map(n_bases, 2 * n_order) 

666 

667 # Pre-compute derivative index transformations 

668 der_indices_even = make_first_even(der_indices) 

669 der_indices_odd = make_first_odd(der_indices) 

670 der_indices_tr_even, der_ind_order_even = transform_der_indices(der_indices_even, der_map) 

671 der_indices_tr_odd, der_ind_order_odd = transform_der_indices(der_indices_odd, der_map) 

672 

673 # Convert index lists to numpy arrays for numba 

674 index_arrays = [np.asarray(locs, dtype=np.int64) for locs in index] 

675 

676 # Compute block offsets 

677 row_offsets = [0, PHIrows] 

678 for i in range(total_derivs): 

679 row_offsets.append(row_offsets[-1] + len(index[i])) 

680 

681 col_offsets = [0, PHIcols] 

682 for i in range(total_derivs): 

683 col_offsets.append(col_offsets[-1] + len(index[i])) 

684 

685 # Allocate output matrix 

686 K = np.zeros((n_output_rows, n_output_cols)) 

687 

688 # Fill blocks 

689 for i in range(total_derivs + 1): 

690 for j in range(total_derivs + 1): 

691 

692 if i == 0 and j == 0: 

693 # K_ff: Full function-function block 

694 K[0:PHIrows, 0:PHIcols] = phi_exp[0] 

695 

696 elif i == 0 and j > 0: 

697 # K_fd: Function rows, derivative j columns 

698 idx = der_indices_tr_even[j - 1] 

699 col_locs = index_arrays[j - 1] 

700 col_start = col_offsets[j] 

701 

702 # Use numba for efficient column extraction 

703 extract_cols_and_assign(phi_exp[idx], col_locs, K, 

704 0, col_start, PHIrows) 

705 

706 elif i > 0 and j == 0: 

707 # K_df: Derivative i rows, function columns 

708 idx = der_indices_tr_odd[i - 1] 

709 row_locs = index_arrays[i - 1] 

710 row_start = row_offsets[i] 

711 

712 # Use numba for efficient row extraction 

713 extract_rows_and_assign(phi_exp[idx], row_locs, K, 

714 row_start, 0, PHIcols) 

715 

716 else: 

717 # K_dd: Derivative i rows, derivative j columns 

718 imdir1 = der_ind_order_even[j - 1] 

719 imdir2 = der_ind_order_odd[i - 1] 

720 new_idx, new_ord = dh.mult_dir( 

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

722 idx = der_map[new_ord][new_idx] 

723 

724 row_locs = index_arrays[i - 1] 

725 col_locs = index_arrays[j - 1] 

726 row_start = row_offsets[i] 

727 col_start = col_offsets[j] 

728 

729 # Use numba for efficient submatrix extraction (replaces np.ix_) 

730 extract_and_assign(phi_exp[idx], row_locs, col_locs, K, 

731 row_start, col_start) 

732 

733 return K 

734 

735 

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

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

738 fd_flat_indices, df_flat_indices, dd_flat_indices, 

739 idx_flat, idx_offsets, idx_sizes, 

740 n_deriv_types, row_offsets, col_offsets): 

741 """Fused numba kernel for GDDEGP K matrix assembly (no signs, even/odd bases).""" 

742 # ff block 

743 for r in range(n_rows_func): 

744 for c in range(n_cols_func): 

745 K[r, c] = phi_exp_3d[0, r, c] 

746 # fd block (even indices) 

747 for j in range(n_deriv_types): 

748 fi = fd_flat_indices[j] 

749 co = col_offsets[j] 

750 off_j = idx_offsets[j] 

751 sz_j = idx_sizes[j] 

752 for r in range(n_rows_func): 

753 for k in range(sz_j): 

754 ci = idx_flat[off_j + k] 

755 K[r, co + k] = phi_exp_3d[fi, r, ci] 

756 # df block (odd indices) 

757 for i in range(n_deriv_types): 

758 fi = df_flat_indices[i] 

759 ro = row_offsets[i] 

760 off_i = idx_offsets[i] 

761 sz_i = idx_sizes[i] 

762 for k in range(sz_i): 

763 ri = idx_flat[off_i + k] 

764 for c in range(n_cols_func): 

765 K[ro + k, c] = phi_exp_3d[fi, ri, c] 

766 # dd block (even × odd) 

767 for i in range(n_deriv_types): 

768 ro = row_offsets[i] 

769 off_i = idx_offsets[i] 

770 sz_i = idx_sizes[i] 

771 for j in range(n_deriv_types): 

772 fi = dd_flat_indices[i, j] 

773 co = col_offsets[j] 

774 off_j = idx_offsets[j] 

775 sz_j = idx_sizes[j] 

776 for ki in range(sz_i): 

777 ri = idx_flat[off_i + ki] 

778 for kj in range(sz_j): 

779 ci = idx_flat[off_j + kj] 

780 K[ro + ki, co + kj] = phi_exp_3d[fi, ri, ci] 

781 

782 

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

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

785 fd_flat_indices, df_flat_indices, dd_flat_indices, 

786 idx_flat, idx_offsets, idx_sizes, 

787 n_deriv_types, row_offsets, col_offsets): 

788 """ 

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

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

791 No-signs variant for GDDEGP even/odd bases. 

792 """ 

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

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

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

796 W_proj[d, r, c] = 0.0 

797 for r in range(n_rows_func): 

798 for c in range(n_cols_func): 

799 W_proj[0, r, c] += W[r, c] 

800 for j in range(n_deriv_types): 

801 fi = fd_flat_indices[j] 

802 co = col_offsets[j] 

803 off_j = idx_offsets[j] 

804 sz_j = idx_sizes[j] 

805 for r in range(n_rows_func): 

806 for k in range(sz_j): 

807 ci = idx_flat[off_j + k] 

808 W_proj[fi, r, ci] += W[r, co + k] 

809 for i in range(n_deriv_types): 

810 fi = df_flat_indices[i] 

811 ro = row_offsets[i] 

812 off_i = idx_offsets[i] 

813 sz_i = idx_sizes[i] 

814 for k in range(sz_i): 

815 ri = idx_flat[off_i + k] 

816 for c in range(n_cols_func): 

817 W_proj[fi, ri, c] += W[ro + k, c] 

818 for i in range(n_deriv_types): 

819 ro = row_offsets[i] 

820 off_i = idx_offsets[i] 

821 sz_i = idx_sizes[i] 

822 for j in range(n_deriv_types): 

823 fi = dd_flat_indices[i, j] 

824 co = col_offsets[j] 

825 off_j = idx_offsets[j] 

826 sz_j = idx_sizes[j] 

827 for ki in range(sz_i): 

828 ri = idx_flat[off_i + ki] 

829 for kj in range(sz_j): 

830 ci = idx_flat[off_j + kj] 

831 W_proj[fi, ri, ci] += W[ro + ki, co + kj] 

832 

833 

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

835 """Precompute structural info for rbf_kernel_fast (GDDEGP even/odd variant).""" 

836 dh = coti.get_dHelp() 

837 assert n_bases % 2 == 0, "n_bases must be an even number." 

838 der_map = deriv_map(n_bases, 2 * n_order) 

839 

840 n_deriv_types = len(der_indices) 

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

842 

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

844 n_pts_with_derivs = int(index_sizes.sum()) 

845 

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

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

848 for i in range(1, n_deriv_types): 

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

850 

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

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

853 cumsum = 0 

854 for i in range(n_deriv_types): 

855 row_offsets[i] = cumsum 

856 col_offsets[i] = cumsum 

857 cumsum += index_sizes[i] 

858 

859 # Even/odd derivative transforms 

860 der_indices_even = make_first_even(der_indices) 

861 der_indices_odd = make_first_odd(der_indices) 

862 der_indices_tr_even, der_ind_order_even = transform_der_indices(der_indices_even, der_map) 

863 der_indices_tr_odd, der_ind_order_odd = transform_der_indices(der_indices_odd, der_map) 

864 

865 fd_flat_indices = np.array(der_indices_tr_even, dtype=np.int64) 

866 df_flat_indices = np.array(der_indices_tr_odd, dtype=np.int64) 

867 

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

869 for i in range(n_deriv_types): 

870 for j in range(n_deriv_types): 

871 imdir1 = der_ind_order_even[j] 

872 imdir2 = der_ind_order_odd[i] 

873 new_idx, new_ord = dh.mult_dir(imdir1[0], imdir1[1], imdir2[0], imdir2[1]) 

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

875 

876 return { 

877 'signs': np.ones(n_deriv_types + 1, dtype=np.float64), # unused, kept for API 

878 'index_arrays': index_arrays, 

879 'index_sizes': index_sizes, 

880 'n_pts_with_derivs': n_pts_with_derivs, 

881 'dd_flat_indices': dd_flat_indices, 

882 'n_deriv_types': n_deriv_types, 

883 'idx_flat': idx_flat, 

884 'idx_offsets': idx_offsets, 

885 'row_offsets': row_offsets, 

886 'col_offsets': col_offsets, 

887 'fd_flat_indices': fd_flat_indices, 

888 'df_flat_indices': df_flat_indices, 

889 } 

890 

891 

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

893 """Fast kernel assembly using precomputed plan and fused numba kernel.""" 

894 n_rows_func = phi_exp_3d.shape[1] 

895 n_cols_func = phi_exp_3d.shape[2] 

896 total = n_rows_func + plan['n_pts_with_derivs'] 

897 if out is not None: 

898 K = out 

899 else: 

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

901 

902 if 'row_offsets_abs' in plan: 

903 row_off = plan['row_offsets_abs'] 

904 col_off = plan['col_offsets_abs'] 

905 else: 

906 row_off = plan['row_offsets'] + n_rows_func 

907 col_off = plan['col_offsets'] + n_cols_func 

908 

909 _assemble_kernel_numba( 

910 phi_exp_3d, K, n_rows_func, n_cols_func, 

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

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

913 plan['n_deriv_types'], row_off, col_off, 

914 ) 

915 return K 

916 

917 

918@profile 

919def rbf_kernel_predictions( 

920 phi, 

921 phi_exp, 

922 n_order, 

923 n_bases, 

924 der_indices, 

925 return_deriv, 

926 index=None, 

927 common_derivs=None, 

928 calc_cov=False, 

929): 

930 """ 

931 Constructs the RBF kernel matrix for predictions with selective derivative coverage. 

932  

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

934 

935 Parameters 

936 ---------- 

937 phi : OTI array 

938 Base kernel matrix between test and training points. 

939 phi_exp : ndarray 

940 Expanded derivative array from phi.get_all_derivs(). 

941 n_order : int 

942 Maximum derivative order. 

943 n_bases : int 

944 Number of OTI bases. 

945 der_indices : list 

946 Derivative specifications for training data. 

947 return_deriv : bool 

948 If True, predict derivatives at test points. 

949 index : list of list 

950 Training point indices for each derivative type. 

951 common_derivs : list 

952 Common derivative indices to predict. 

953 calc_cov : bool 

954 If True, computing covariance. 

955 

956 Returns 

957 ------- 

958 K : ndarray 

959 Prediction kernel matrix. 

960 """ 

961 # Early return for covariance-only case 

962 if calc_cov and not return_deriv: 

963 return phi.real.T 

964 

965 dh = coti.get_dHelp() 

966 

967 n_train, n_test = phi.shape 

968 n_deriv_types = len(der_indices) 

969 n_deriv_types_pred = len(common_derivs) if common_derivs else 0 

970 

971 # Handle n_order = 0 case 

972 if n_order == 0: 

973 return phi.real.T 

974 

975 # Convert index lists to numpy arrays for numba 

976 index_arrays = [np.asarray(locs, dtype=np.int64) for locs in index] 

977 

978 # Determine derivative map 

979 if return_deriv: 

980 der_map = deriv_map(n_bases, 2 * n_order) 

981 derivative_locations_test = [np.arange(n_test, dtype=np.int64)] * n_deriv_types_pred 

982 else: 

983 der_map = deriv_map(n_bases, n_order) 

984 

985 # Create derivative index transformations 

986 der_indices_even = make_first_even(der_indices) 

987 der_indices_odd = make_first_odd(der_indices) 

988 der_indices_tr_odd, der_ind_order_odd = transform_der_indices(der_indices_odd, der_map) 

989 der_indices_odd_pred = make_first_odd(common_derivs) if common_derivs else [] 

990 der_indices_tr_odd_pred, der_ind_order_odd_pred = transform_der_indices(der_indices_odd_pred, der_map) if common_derivs else ([], []) 

991 

992 # Compute matrix dimensions 

993 n_rows_func = n_test 

994 if return_deriv: 

995 n_rows_derivs = sum(len(locs) for locs in derivative_locations_test) 

996 else: 

997 n_rows_derivs = 0 

998 total_rows = n_rows_func + n_rows_derivs 

999 

1000 if return_deriv and calc_cov: 

1001 n_deriv_types = n_deriv_types_pred 

1002 n_cols_derivs = sum(len(locs) for locs in derivative_locations_test) 

1003 total_cols = n_train + n_cols_derivs 

1004 else: 

1005 n_cols_derivs = sum(len(locs) for locs in index) 

1006 total_cols = n_train + n_cols_derivs 

1007 

1008 # Compute block offsets 

1009 row_offsets = [0, n_test] 

1010 if return_deriv: 

1011 for i in range(n_deriv_types_pred): 

1012 row_offsets.append(row_offsets[-1] + len(derivative_locations_test[i])) 

1013 

1014 col_offsets = [0, n_train] 

1015 if return_deriv and calc_cov: 

1016 for i in range(n_deriv_types): 

1017 col_offsets.append(col_offsets[-1] + len(derivative_locations_test[i])) 

1018 else: 

1019 for i in range(n_deriv_types): 

1020 col_offsets.append(col_offsets[-1] + len(index[i])) 

1021 

1022 # Allocate output matrix 

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

1024 base_shape = (n_train, n_test) 

1025 

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

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

1028 K[:n_test, :n_train] = content_full.T 

1029 

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

1031 for j in range(n_deriv_types): 

1032 col_locs = derivative_locations_test[j] if (return_deriv and calc_cov) else index_arrays[j] 

1033 col_start = col_offsets[j + 1] 

1034 

1035 flat_idx = der_indices_tr_odd_pred[j] if calc_cov else der_indices_tr_odd[j] 

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

1037 

1038 # Use numba for efficient row extraction with transpose 

1039 extract_rows_and_assign_transposed(content_full, col_locs, K, 

1040 0, col_start, n_test) 

1041 

1042 if not return_deriv: 

1043 return K 

1044 

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

1046 der_indices_tr_even, der_ind_order_even = transform_der_indices(der_indices_even, der_map) 

1047 der_indices_even_pred = make_first_even(common_derivs) 

1048 der_indices_tr_even_pred, der_ind_order_even_pred = transform_der_indices(der_indices_even_pred, der_map) 

1049 

1050 for i in range(n_deriv_types_pred): 

1051 test_locs = derivative_locations_test[i] 

1052 row_start = row_offsets[i + 1] 

1053 

1054 flat_idx = der_indices_tr_even_pred[i] 

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

1056 

1057 # Use numba for efficient column extraction with transpose 

1058 extract_cols_and_assign_transposed(content_full, test_locs, K, 

1059 row_start, 0, n_train) 

1060 

1061 # Inner Blocks: Derivative-Derivative (K_dd) 

1062 for i in range(n_deriv_types_pred): 

1063 test_locs = derivative_locations_test[i] 

1064 row_start = row_offsets[i + 1] 

1065 

1066 for j in range(n_deriv_types): 

1067 col_locs = derivative_locations_test[j] if (return_deriv and calc_cov) else index_arrays[j] 

1068 col_start = col_offsets[j + 1] 

1069 

1070 imdir_train = der_ind_order_odd_pred[j] if calc_cov else der_ind_order_odd[j] 

1071 imdir_test = der_ind_order_even_pred[i] 

1072 new_idx, new_ord = dh.mult_dir( 

1073 imdir_train[0], imdir_train[1], 

1074 imdir_test[0], imdir_test[1] 

1075 ) 

1076 flat_idx = der_map[new_ord][new_idx] 

1077 

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

1079 

1080 # Use numba for efficient submatrix extraction with transpose (replaces np.ix_ + .T) 

1081 extract_and_assign_transposed(content_full, col_locs, test_locs, K, 

1082 row_start, col_start) 

1083 

1084 return K