1use crate::errors::{GpError, Result};
2use crate::mean_models::*;
3use crate::optimization::{CobylaParams, optimize_params, prepare_multistart};
4use crate::parameters::{GpParams, GpValidParams};
5use crate::utils::{DiffMatrix, NormalizedData, pairwise_differences};
6use crate::{ThetaTuning, correlation_models::*};
7
8use linfa::dataset::{WithLapack, WithoutLapack};
9use linfa::prelude::{Dataset, DatasetBase, Fit, Float, PredictInplace};
10
11#[cfg(not(feature = "blas"))]
12use linfa_linalg::{cholesky::*, eigh::*, qr::*, svd::*, triangular::*};
13#[cfg(feature = "blas")]
14use log::warn;
15#[cfg(feature = "blas")]
16use ndarray_linalg::{cholesky::*, eigh::*, qr::*, svd::*, triangular::*};
17
18use linfa_pls::PlsRegression;
19use ndarray::{Array, Array1, Array2, ArrayBase, Axis, Data, Ix1, Ix2, Zip};
20
21use ndarray_rand::RandomExt;
22use ndarray_rand::rand_distr::Normal;
23use ndarray_stats::QuantileExt;
24
25use log::debug;
26use rayon::prelude::*;
27#[cfg(feature = "serializable")]
28use serde::{Deserialize, Serialize};
29use std::fmt;
30use std::time::Instant;
31
32pub const GP_OPTIM_N_START: usize = 10;
34pub const GP_COBYLA_MIN_EVAL: usize = 25;
36pub const GP_COBYLA_MAX_EVAL: usize = 1000;
38
39#[derive(Default, Debug)]
42#[cfg_attr(
43 feature = "serializable",
44 derive(Serialize, Deserialize),
45 serde(bound(deserialize = "F: Deserialize<'de>"))
46)]
47pub(crate) struct GpInnerParams<F: Float> {
48 sigma2: F,
50 beta: Array2<F>,
52 gamma: Array2<F>,
54 r_chol: Array2<F>,
56 ft: Array2<F>,
58 ft_qr_r: Array2<F>,
60}
61
62impl<F: Float> Clone for GpInnerParams<F> {
63 fn clone(&self) -> Self {
64 Self {
65 sigma2: self.sigma2.to_owned(),
66 beta: self.beta.to_owned(),
67 gamma: self.gamma.to_owned(),
68 r_chol: self.r_chol.to_owned(),
69 ft: self.ft.to_owned(),
70 ft_qr_r: self.ft_qr_r.to_owned(),
71 }
72 }
73}
74
75#[derive(Debug)]
166#[cfg_attr(
167 feature = "serializable",
168 derive(Serialize, Deserialize),
169 serde(bound(
170 serialize = "F: Serialize, Mean: Serialize, Corr: Serialize",
171 deserialize = "F: Deserialize<'de>, Mean: Deserialize<'de>, Corr: Deserialize<'de>"
172 ))
173)]
174pub struct GaussianProcess<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> {
175 theta: Array1<F>,
177 likelihood: F,
180 inner_params: GpInnerParams<F>,
182 w_star: Array2<F>,
184 xt_norm: NormalizedData<F>,
186 yt_norm: NormalizedData<F>,
188 pub(crate) training_data: (Array2<F>, Array1<F>),
190 pub(crate) params: GpValidParams<F, Mean, Corr>,
192}
193
194pub(crate) enum GpSamplingMethod {
195 Cholesky,
196 EigenValues,
197}
198
199pub type Kriging<F> = GpParams<F, ConstantMean, SquaredExponentialCorr>;
201
202impl<F: Float> Kriging<F> {
203 pub fn params() -> GpParams<F, ConstantMean, SquaredExponentialCorr> {
205 GpParams::new(ConstantMean(), SquaredExponentialCorr())
206 }
207}
208
209impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> Clone
210 for GaussianProcess<F, Mean, Corr>
211{
212 fn clone(&self) -> Self {
213 Self {
214 theta: self.theta.to_owned(),
215 likelihood: self.likelihood,
216 inner_params: self.inner_params.clone(),
217 w_star: self.w_star.to_owned(),
218 xt_norm: self.xt_norm.clone(),
219 yt_norm: self.yt_norm.clone(),
220 training_data: self.training_data.clone(),
221 params: self.params.clone(),
222 }
223 }
224}
225
226impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> fmt::Display
227 for GaussianProcess<F, Mean, Corr>
228{
229 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
230 write!(
231 f,
232 "GP(mean={}, corr={}, theta={}, variance={}, likelihood={})",
233 self.params.mean,
234 self.params.corr,
235 self.theta,
236 self.inner_params.sigma2,
237 self.likelihood,
238 )
239 }
240}
241
242impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>> GaussianProcess<F, Mean, Corr> {
243 pub fn params<NewMean: RegressionModel<F>, NewCorr: CorrelationModel<F>>(
245 mean: NewMean,
246 corr: NewCorr,
247 ) -> GpParams<F, NewMean, NewCorr> {
248 GpParams::new(mean, corr)
249 }
250
251 pub fn predict(&self, x: &ArrayBase<impl Data<Elem = F>, Ix2>) -> Result<Array1<F>> {
254 let xnorm = (x - &self.xt_norm.mean) / &self.xt_norm.std;
255 let f = self.params.mean.coefs(&xnorm);
257 let corr = self._compute_correlation(&xnorm);
259 let y_ = &f.dot(&self.inner_params.beta) + &corr.dot(&self.inner_params.gamma);
261 Ok((&y_ * &self.yt_norm.std + &self.yt_norm.mean).remove_axis(Axis(1)))
263 }
264
265 pub fn predict_var(&self, x: &ArrayBase<impl Data<Elem = F>, Ix2>) -> Result<Array1<F>> {
268 let xnorm = (x - &self.xt_norm.mean) / &self.xt_norm.std;
269 let corr = self._compute_correlation(&xnorm);
270 let (rt, u) = self._compute_rt_u(&xnorm, &corr);
271
272 let mut mse = Array::ones(rt.ncols()) - rt.mapv(|v| v * v).sum_axis(Axis(0))
273 + u.mapv(|v: F| v * v).sum_axis(Axis(0));
274 mse.mapv_inplace(|v| self.inner_params.sigma2 * v);
275
276 Ok(mse.mapv(|v| if v < F::zero() { F::zero() } else { F::cast(v) }))
279 }
280
281 pub fn predict_valvar(
283 &self,
284 x: &ArrayBase<impl Data<Elem = F>, Ix2>,
285 ) -> Result<(Array1<F>, Array1<F>)> {
286 let xnorm = (x - &self.xt_norm.mean) / &self.xt_norm.std;
287 let f = self.params.mean.coefs(&xnorm);
289 let corr = self._compute_correlation(&xnorm);
291 let y_ = &f.dot(&self.inner_params.beta) + &corr.dot(&self.inner_params.gamma);
293 let yp = (&y_ * &self.yt_norm.std + &self.yt_norm.mean).remove_axis(Axis(1));
295
296 let (rt, u) = self._compute_rt_u(&xnorm, &corr);
297
298 let mut mse = Array::ones(rt.ncols()) - rt.mapv(|v| v * v).sum_axis(Axis(0))
299 + u.mapv(|v: F| v * v).sum_axis(Axis(0));
300 mse.mapv_inplace(|v| self.inner_params.sigma2 * v);
301
302 let vmse = mse.mapv(|v| if v < F::zero() { F::zero() } else { F::cast(v) });
305
306 Ok((yp, vmse))
307 }
308
309 fn _compute_covariance(&self, x: &ArrayBase<impl Data<Elem = F>, Ix2>) -> Array2<F> {
311 let xnorm = (x - &self.xt_norm.mean) / &self.xt_norm.std;
312 let corr = self._compute_correlation(&xnorm);
313 let (rt, u) = self._compute_rt_u(&xnorm, &corr);
314
315 let cross_dx = pairwise_differences(&xnorm, &xnorm);
316 let k = self
317 .params
318 .corr
319 .rval_from_distances(&cross_dx, &self.theta, &self.w_star);
320 let k = k
321 .into_shape_with_order((xnorm.nrows(), xnorm.nrows()))
322 .unwrap();
323
324 let mut cov_matrix = k - rt.t().to_owned().dot(&rt) + u.t().dot(&u);
327 cov_matrix.mapv_inplace(|v| self.inner_params.sigma2 * v);
328 cov_matrix
329 }
330
331 fn _compute_rt_u(
334 &self,
335 xnorm: &ArrayBase<impl Data<Elem = F>, Ix2>,
336 corr: &ArrayBase<impl Data<Elem = F>, Ix2>,
337 ) -> (Array2<F>, Array2<F>) {
338 let inners = &self.inner_params;
339
340 let corr_t = corr.t().to_owned();
341 #[cfg(feature = "blas")]
342 let rt = inners
343 .r_chol
344 .to_owned()
345 .with_lapack()
346 .solve_triangular(UPLO::Lower, Diag::NonUnit, &corr_t.with_lapack())
347 .unwrap()
348 .without_lapack();
349 #[cfg(not(feature = "blas"))]
350 let rt = inners
351 .r_chol
352 .solve_triangular(&corr_t, UPLO::Lower)
353 .unwrap();
354
355 let rhs = inners.ft.t().dot(&rt) - self.params.mean.coefs(xnorm).t();
356 #[cfg(feature = "blas")]
357 let u = inners
358 .ft_qr_r
359 .to_owned()
360 .t()
361 .with_lapack()
362 .solve_triangular(UPLO::Upper, Diag::NonUnit, &rhs.with_lapack())
363 .unwrap()
364 .without_lapack();
365 #[cfg(not(feature = "blas"))]
366 let u = inners
367 .ft_qr_r
368 .t()
369 .solve_triangular(&rhs, UPLO::Lower)
370 .unwrap();
371 (rt, u)
372 }
373
374 fn _compute_correlation(&self, xnorm: &ArrayBase<impl Data<Elem = F>, Ix2>) -> Array2<F> {
376 let dx = pairwise_differences(xnorm, &self.xt_norm.data);
378 let r = self
380 .params
381 .corr
382 .rval_from_distances(&dx, &self.theta, &self.w_star);
383 let n_obs = xnorm.nrows();
384 let nt = self.xt_norm.data.nrows();
385 r.into_shape_with_order((n_obs, nt)).unwrap().to_owned()
386 }
387
388 pub fn sample_chol(&self, x: &ArrayBase<impl Data<Elem = F>, Ix2>, n_traj: usize) -> Array2<F> {
390 self._sample(x, n_traj, GpSamplingMethod::Cholesky)
391 }
392
393 pub fn sample_eig(&self, x: &ArrayBase<impl Data<Elem = F>, Ix2>, n_traj: usize) -> Array2<F> {
395 self._sample(x, n_traj, GpSamplingMethod::EigenValues)
396 }
397
398 pub fn sample(&self, x: &ArrayBase<impl Data<Elem = F>, Ix2>, n_traj: usize) -> Array2<F> {
400 self.sample_eig(x, n_traj)
401 }
402
403 fn _sample(
408 &self,
409 x: &ArrayBase<impl Data<Elem = F>, Ix2>,
410 n_traj: usize,
411 method: GpSamplingMethod,
412 ) -> Array2<F> {
413 let mean = self.predict(x).unwrap();
414 let cov = self._compute_covariance(x);
415 sample(x, mean.insert_axis(Axis(1)), cov, n_traj, method)
416 }
417
418 pub fn theta(&self) -> &Array1<F> {
420 &self.theta
421 }
422
423 pub fn variance(&self) -> F {
425 self.inner_params.sigma2
426 }
427
428 pub fn likelihood(&self) -> F {
430 self.likelihood
431 }
432
433 pub fn kpls_dim(&self) -> Option<usize> {
435 if self.w_star.ncols() < self.xt_norm.ncols() {
436 Some(self.w_star.ncols())
437 } else {
438 None
439 }
440 }
441
442 pub fn dims(&self) -> (usize, usize) {
444 (self.xt_norm.ncols(), self.yt_norm.ncols())
445 }
446
447 pub fn predict_kth_derivatives(
450 &self,
451 x: &ArrayBase<impl Data<Elem = F>, Ix2>,
452 kx: usize,
453 ) -> Array1<F> {
454 let xnorm = (x - &self.xt_norm.mean) / &self.xt_norm.std;
455 let corr = self._compute_correlation(&xnorm);
456
457 let beta = &self.inner_params.beta;
458 let gamma = &self.inner_params.gamma;
459
460 let df_dx_kx = if self.inner_params.beta.nrows() <= 1 + self.xt_norm.data.ncols() {
461 let df = self.params.mean.jac(&x.row(0));
463 let df_dx = df.t().row(kx).dot(beta);
464 df_dx.broadcast((x.nrows(), 1)).unwrap().to_owned()
465 } else {
466 let mut dfdx = Array2::zeros((x.nrows(), 1));
468 Zip::from(dfdx.rows_mut())
469 .and(xnorm.rows())
470 .for_each(|mut dfxi, xi| {
471 let df = self.params.mean.jac(&xi);
472 let df_dx = (df.t().row(kx)).dot(beta);
473 dfxi.assign(&df_dx);
474 });
475 dfdx
476 };
477
478 let nr = x.nrows();
479 let nc = self.xt_norm.data.nrows();
480 let d_dx_1 = &xnorm
481 .column(kx)
482 .to_owned()
483 .into_shape_with_order((nr, 1))
484 .unwrap()
485 .broadcast((nr, nc))
486 .unwrap()
487 .to_owned();
488
489 let d_dx_2 = self
490 .xt_norm
491 .data
492 .column(kx)
493 .to_owned()
494 .as_standard_layout()
495 .into_shape_with_order((1, nc))
496 .unwrap()
497 .to_owned();
498
499 let d_dx = d_dx_1 - d_dx_2;
500
501 let theta = &self.theta.to_owned();
503 let d_dx_corr = d_dx * corr;
504
505 let res = (df_dx_kx - d_dx_corr.dot(gamma).map(|v| F::cast(2.) * theta[kx] * *v))
509 * self.yt_norm.std[0]
510 / self.xt_norm.std[kx];
511 res.column(0).to_owned()
512 }
513
514 pub fn predict_gradients(&self, x: &ArrayBase<impl Data<Elem = F>, Ix2>) -> Array2<F> {
517 let mut drv = Array2::<F>::zeros((x.nrows(), self.xt_norm.data.ncols()));
518 Zip::from(drv.rows_mut())
519 .and(x.rows())
520 .for_each(|mut row, xi| {
521 let pred = self.predict_jacobian(&xi);
522 row.assign(&pred.column(0));
523 });
524 drv
525 }
526
527 fn predict_jacobian(&self, x: &ArrayBase<impl Data<Elem = F>, Ix1>) -> Array2<F> {
530 let xx = x.to_owned().insert_axis(Axis(0));
531 let mut jac = Array2::zeros((xx.ncols(), 1));
532
533 let xnorm = (xx - &self.xt_norm.mean) / &self.xt_norm.std;
534
535 let beta = &self.inner_params.beta;
536 let gamma = &self.inner_params.gamma;
537
538 let df = self.params.mean.jac(&xnorm.row(0));
539 let df_dx = df.t().dot(beta);
540
541 let dr = self
542 .params
543 .corr
544 .jac(&xnorm.row(0), &self.xt_norm.data, &self.theta, &self.w_star);
545
546 let dr_dx = df_dx + dr.t().dot(gamma);
547 Zip::from(jac.rows_mut())
548 .and(dr_dx.rows())
549 .and(&self.xt_norm.std)
550 .for_each(|mut jc, dr_i, std_i| {
551 let jc_i = dr_i.map(|v| *v * self.yt_norm.std[0] / *std_i);
552 jc.assign(&jc_i)
553 });
554
555 jac
556 }
557
558 #[cfg(not(feature = "blas"))]
561 pub fn predict_var_gradients_single(
562 &self,
563 x: &ArrayBase<impl Data<Elem = F>, Ix1>,
564 ) -> Array1<F> {
565 let x = &(x.to_owned().insert_axis(Axis(0)));
566 let xnorm = (x - &self.xt_norm.mean) / &self.xt_norm.std;
567 let sigma2 = self.inner_params.sigma2;
568 let r_chol = &self.inner_params.r_chol;
569
570 let (r, dr) = self.params.corr.rval_with_jac(
571 &xnorm.row(0),
572 &self.xt_norm.data,
573 &self.theta,
574 &self.w_star,
575 );
576
577 let rho1 = r_chol.solve_triangular(&r, UPLO::Lower).unwrap();
579
580 let inv_kr = r_chol.t().solve_triangular(&rho1, UPLO::Upper).unwrap();
582
583 let p2 = inv_kr.t().dot(&dr);
588
589 let f_x = self.params.mean.coefs(&xnorm).t().to_owned();
590 let f_mean = self.params.mean.coefs(&self.xt_norm.data);
591
592 let rho2 = r_chol.solve_triangular(&f_mean, UPLO::Lower).unwrap();
594 let inv_kf = r_chol.t().solve_triangular(&rho2, UPLO::Upper).unwrap();
596
597 let a_mat = f_x.t().to_owned() - r.t().dot(&inv_kf);
599
600 let b_mat = f_mean.t().dot(&inv_kf);
602 let rho3 = b_mat.cholesky().unwrap();
604 let inv_bat = rho3.solve_triangular(&a_mat.t(), UPLO::Lower).unwrap();
606 let d_mat = rho3.t().solve_triangular(&inv_bat, UPLO::Upper).unwrap();
608
609 let df = self.params.mean.jac(&xnorm.row(0));
610
611 let d_a = df.t().to_owned() - dr.t().dot(&inv_kf);
613
614 let p4 = d_mat.t().dot(&d_a.t());
619 let two = F::cast(2.);
620 let prime = (p4 - p2).mapv(|v| two * v);
621
622 let x_std = &self.xt_norm.std;
623 let dvar = (prime / x_std).mapv(|v| v * sigma2);
624 dvar.row(0).into_owned()
625 }
626
627 #[cfg(feature = "blas")]
629 pub fn predict_var_gradients_single(
630 &self,
631 x: &ArrayBase<impl Data<Elem = F>, Ix1>,
632 ) -> Array1<F> {
633 let x = &(x.to_owned().insert_axis(Axis(0)));
634 let xnorm = (x - &self.xt_norm.mean) / &self.xt_norm.std;
635
636 let dx = pairwise_differences(&xnorm, &self.xt_norm.data);
637
638 let sigma2 = self.inner_params.sigma2;
639 let r_chol = &self.inner_params.r_chol.to_owned().with_lapack();
640
641 let r = self
642 .params
643 .corr
644 .rval_from_distances(&dx, &self.theta, &self.w_star)
645 .with_lapack();
646 let dr = self
647 .params
648 .corr
649 .jac(&xnorm.row(0), &self.xt_norm.data, &self.theta, &self.w_star)
650 .with_lapack();
651
652 let rho1 = r_chol
653 .solve_triangular(UPLO::Lower, Diag::NonUnit, &r)
654 .unwrap();
655 let inv_kr = r_chol
656 .t()
657 .solve_triangular(UPLO::Upper, Diag::NonUnit, &rho1)
658 .unwrap();
659
660 let p2 = inv_kr.t().dot(&dr);
663
664 let f_x = self.params.mean.coefs(x).t().to_owned();
665 let f_mean = self.params.mean.coefs(&self.xt_norm.data).with_lapack();
666
667 let rho2 = r_chol
668 .solve_triangular(UPLO::Lower, Diag::NonUnit, &f_mean)
669 .unwrap();
670 let inv_kf = r_chol
671 .t()
672 .solve_triangular(UPLO::Upper, Diag::NonUnit, &rho2)
673 .unwrap();
674
675 let a_mat = f_x.t().to_owned().with_lapack() - r.t().dot(&inv_kf);
676
677 let b_mat = f_mean.t().dot(&inv_kf);
678
679 let d_mat = match b_mat.cholesky(UPLO::Lower) {
680 Ok(rho3) => {
681 let inv_bat = rho3
682 .solve_triangular(UPLO::Upper, Diag::NonUnit, &a_mat.t().to_owned())
683 .unwrap();
684 rho3.t()
685 .solve_triangular(UPLO::Upper, Diag::NonUnit, &inv_bat)
686 .unwrap()
687 }
688 Err(_) => {
689 warn!("Cholesky decomposition error during variance dervivatives computation");
690 Array2::zeros((b_mat.nrows(), b_mat.ncols()))
691 }
692 };
693
694 let df = self.params.mean.jac(&xnorm.row(0)).with_lapack();
695
696 let d_a = df.t().to_owned() - dr.t().dot(&inv_kf);
697 let p4 = d_mat.t().dot(&d_a.t());
699
700 let two = F::cast(2.);
701 let prime_t = (p4 - p2).without_lapack().mapv(|v| two * v);
702
703 let x_std = &self.xt_norm.std;
704 let dvar = (prime_t / x_std).mapv(|v| v * sigma2);
705 dvar.row(0).into_owned()
706 }
707
708 pub fn predict_var_gradients(&self, x: &ArrayBase<impl Data<Elem = F>, Ix2>) -> Array2<F> {
711 let mut derivs = Array::zeros((x.nrows(), x.ncols()));
712 Zip::from(derivs.rows_mut())
713 .and(x.rows())
714 .for_each(|mut der, x| der.assign(&self.predict_var_gradients_single(&x)));
715 derivs
716 }
717
718 pub fn predict_valvar_gradients(
721 &self,
722 x: &ArrayBase<impl Data<Elem = F>, Ix2>,
723 ) -> (Array2<F>, Array2<F>) {
724 let mut val_derivs = Array::zeros((x.nrows(), x.ncols()));
725 let mut var_derivs = Array::zeros((x.nrows(), x.ncols()));
726 Zip::from(val_derivs.rows_mut())
727 .and(var_derivs.rows_mut())
728 .and(x.rows())
729 .for_each(|mut val_der, mut var_der, x| {
730 val_der.assign(&self.predict_jacobian(&x).column(0));
731 var_der.assign(&self.predict_var_gradients_single(&x));
732 });
733 (val_derivs, var_derivs)
734 }
735}
736
737impl<F, D, Mean, Corr> PredictInplace<ArrayBase<D, Ix2>, Array1<F>>
738 for GaussianProcess<F, Mean, Corr>
739where
740 F: Float,
741 D: Data<Elem = F>,
742 Mean: RegressionModel<F>,
743 Corr: CorrelationModel<F>,
744{
745 fn predict_inplace(&self, x: &ArrayBase<D, Ix2>, y: &mut Array1<F>) {
746 assert_eq!(
747 x.nrows(),
748 y.len(),
749 "The number of data points must match the number of output targets."
750 );
751
752 let values = self.predict(x).expect("GP Prediction");
753 *y = values;
754 }
755
756 fn default_target(&self, x: &ArrayBase<D, Ix2>) -> Array1<F> {
757 Array1::zeros((x.nrows(),))
758 }
759}
760
761#[allow(dead_code)]
763pub struct GpVariancePredictor<'a, F, Mean, Corr>(&'a GaussianProcess<F, Mean, Corr>)
764where
765 F: Float,
766 Mean: RegressionModel<F>,
767 Corr: CorrelationModel<F>;
768
769impl<F, D, Mean, Corr> PredictInplace<ArrayBase<D, Ix2>, Array1<F>>
770 for GpVariancePredictor<'_, F, Mean, Corr>
771where
772 F: Float,
773 D: Data<Elem = F>,
774 Mean: RegressionModel<F>,
775 Corr: CorrelationModel<F>,
776{
777 fn predict_inplace(&self, x: &ArrayBase<D, Ix2>, y: &mut Array1<F>) {
778 assert_eq!(
779 x.nrows(),
780 y.len(),
781 "The number of data points must match the number of output targets."
782 );
783
784 let values = self.0.predict_var(x).expect("GP Prediction");
785 *y = values;
786 }
787
788 fn default_target(&self, x: &ArrayBase<D, Ix2>) -> Array1<F> {
789 Array1::zeros(x.nrows())
790 }
791}
792
793impl<F: Float, Mean: RegressionModel<F>, Corr: CorrelationModel<F>, D: Data<Elem = F>>
794 Fit<ArrayBase<D, Ix2>, ArrayBase<D, Ix1>, GpError> for GpValidParams<F, Mean, Corr>
795{
796 type Object = GaussianProcess<F, Mean, Corr>;
797
798 fn fit(
800 &self,
801 dataset: &DatasetBase<ArrayBase<D, Ix2>, ArrayBase<D, Ix1>>,
802 ) -> Result<Self::Object> {
803 let x = dataset.records();
804 let y = dataset.targets().to_owned().insert_axis(Axis(1));
805
806 if let Some(d) = self.kpls_dim()
807 && *d > x.ncols()
808 {
809 return Err(GpError::InvalidValueError(format!(
810 "Dimension reduction {} should be smaller than actual \
811 training input dimensions {}",
812 d,
813 x.ncols()
814 )));
815 }
816
817 let dim = if let Some(n_components) = self.kpls_dim() {
818 *n_components
819 } else {
820 x.ncols()
821 };
822
823 let (x, y, active, init) = match self.theta_tuning() {
824 ThetaTuning::Fixed(init) | ThetaTuning::Full { init, bounds: _ } => (
825 x.to_owned(),
826 y.to_owned(),
827 (0..dim).collect::<Vec<_>>(),
828 init,
829 ),
830 ThetaTuning::Partial {
831 init,
832 bounds: _,
833 active,
834 } => (x.to_owned(), y.to_owned(), active.to_vec(), init),
835 };
836 let theta0_dim = init.len();
838 let theta0 = if theta0_dim == 1 {
839 Array1::from_elem(dim, init[0])
840 } else if theta0_dim == dim {
841 init.to_owned()
842 } else {
843 panic!(
844 "Initial guess for theta should be either 1-dim or dim of xtrain (w_star.ncols()), got {theta0_dim}"
845 )
846 };
847
848 let xtrain = NormalizedData::new(&x);
849 let ytrain = NormalizedData::new(&y);
850
851 let mut w_star = Array2::eye(x.ncols());
852 if let Some(n_components) = self.kpls_dim() {
853 let ds = Dataset::new(x.to_owned(), y.to_owned());
854 w_star = PlsRegression::params(*n_components).fit(&ds).map_or_else(
855 |e| match e {
856 linfa_pls::PlsError::PowerMethodConstantResidualError() => {
857 Ok(Array2::zeros((x.ncols(), *n_components)))
858 }
859 err => Err(err),
860 },
861 |v| Ok(v.rotations().0.to_owned()),
862 )?;
863 };
864 let x_distances = DiffMatrix::new(&xtrain.data);
865 let sums = x_distances
866 .d
867 .mapv(|v| num_traits::float::Float::abs(v))
868 .sum_axis(Axis(1));
869 if *sums.min().unwrap() == F::zero() {
870 log::warn!("Warning: multiple x have the same value (at least same row twice).");
871 }
872 let fx = self.mean().coefs(&xtrain.data);
873
874 let opt_params = match self.theta_tuning() {
875 ThetaTuning::Fixed(init) => {
876 init.to_owned()
878 }
879 ThetaTuning::Full { init: _, bounds }
880 | ThetaTuning::Partial {
881 init: _,
882 bounds,
883 active: _,
884 } => {
885 let base: f64 = 10.;
886 let objfn = |x: &[f64], _gradient: Option<&mut [f64]>, _params: &mut ()| -> f64 {
887 let mut theta = theta0.to_owned();
888 let xarr = x.iter().map(|v| base.powf(*v)).collect::<Vec<_>>();
889 std::iter::zip(active.clone(), xarr).for_each(|(i, xi)| theta[i] = F::cast(xi));
890
891 for v in theta.iter() {
892 if v.is_nan() {
894 return f64::INFINITY;
896 }
897 }
898 let rxx = self
899 .corr()
900 .rval_from_distances(&x_distances.d, &theta, &w_star);
901 match reduced_likelihood(&fx, rxx, &x_distances, &ytrain, self.nugget()) {
902 Ok(r) => unsafe { -(*(&r.0 as *const F as *const f64)) },
903 Err(_) => f64::INFINITY,
904 }
905 };
906
907 let bounds_dim = bounds.len();
910 let bounds = if bounds_dim == 1 {
911 vec![bounds[0]; w_star.ncols()]
912 } else if bounds_dim == w_star.ncols() {
913 bounds.to_vec()
914 } else {
915 panic!(
916 "Bounds for theta should be either 1-dim or dim of xtrain ({}), got {}",
917 w_star.ncols(),
918 bounds_dim
919 )
920 };
921
922 let active_bounds = bounds
924 .iter()
925 .enumerate()
926 .filter(|(i, _)| active.contains(i))
927 .map(|(_, &b)| b)
928 .collect::<Vec<_>>();
929 let (theta_inits, bounds) = prepare_multistart(
930 self.n_start(),
931 &theta0.select(Axis(0), &active),
932 &active_bounds,
933 );
934 debug!("Optimize with multistart theta = {theta_inits:?} and bounds = {bounds:?}");
935 let now = Instant::now();
936 let opt_params = (0..theta_inits.nrows())
937 .into_par_iter()
938 .map(|i| {
939 optimize_params(
940 objfn,
941 &theta_inits.row(i).to_owned(),
942 &bounds,
943 CobylaParams {
944 maxeval: (10 * theta_inits.ncols())
945 .clamp(GP_COBYLA_MIN_EVAL, self.max_eval()),
946 ..CobylaParams::default()
947 },
948 )
949 })
950 .reduce(
951 || (f64::INFINITY, Array::ones((theta_inits.ncols(),))),
952 |a, b| if b.0 < a.0 { b } else { a },
953 );
954 debug!("elapsed optim = {:?}", now.elapsed().as_millis());
955 opt_params.1.mapv(|v| F::cast(base.powf(v)))
956 }
957 };
958
959 let opt_params = match self.theta_tuning() {
961 ThetaTuning::Fixed(_) | ThetaTuning::Full { init: _, bounds: _ } => opt_params,
962 ThetaTuning::Partial {
963 init,
964 bounds: _,
965 active,
966 } => {
967 let mut opt_theta = init.to_owned();
968 std::iter::zip(active.clone(), opt_params)
969 .for_each(|(i, xi)| opt_theta[i] = F::cast(xi));
970 opt_theta
971 }
972 };
973
974 let rxx = self
975 .corr()
976 .rval_from_distances(&x_distances.d, &opt_params, &w_star);
977 let (lkh, inner_params) =
978 reduced_likelihood(&fx, rxx, &x_distances, &ytrain, self.nugget())?;
979 Ok(GaussianProcess {
980 theta: opt_params,
981 likelihood: lkh,
982 inner_params,
983 w_star,
984 xt_norm: xtrain,
985 yt_norm: ytrain,
986 training_data: (x.to_owned(), y.to_owned().remove_axis(Axis(1))),
987 params: self.clone(),
988 })
989 }
990}
991
992#[cfg(not(feature = "blas"))]
999fn reduced_likelihood<F: Float>(
1000 fx: &ArrayBase<impl Data<Elem = F>, Ix2>,
1001 rxx: ArrayBase<impl Data<Elem = F>, Ix2>,
1002 x_distances: &DiffMatrix<F>,
1003 ytrain: &NormalizedData<F>,
1004 nugget: F,
1005) -> Result<(F, GpInnerParams<F>)> {
1006 let mut r_mx: Array2<F> = Array2::<F>::eye(x_distances.n_obs).mapv(|v| v + v * nugget);
1008 for (i, ij) in x_distances.d_indices.outer_iter().enumerate() {
1009 r_mx[[ij[0], ij[1]]] = rxx[[i, 0]];
1010 r_mx[[ij[1], ij[0]]] = rxx[[i, 0]];
1011 }
1012 let fxl = fx;
1013 let r_chol = r_mx.cholesky()?;
1015 let ft = r_chol.solve_triangular(fxl, UPLO::Lower)?;
1017 let (ft_qr_q, ft_qr_r) = ft.qr().unwrap().into_decomp();
1018
1019 let (_, sv_qr_r, _) = ft_qr_r.svd(false, false).unwrap();
1021 let cond_ft = sv_qr_r[sv_qr_r.len() - 1] / sv_qr_r[0];
1022 if F::cast(cond_ft) < F::cast(1e-10) {
1023 let (_, sv_f, _) = &fxl.svd(false, false).unwrap();
1024 let cond_fx = sv_f[0] / sv_f[sv_f.len() - 1];
1025 if F::cast(cond_fx) > F::cast(1e15) {
1026 return Err(GpError::LikelihoodComputationError(
1027 "F is too ill conditioned. Poor combination \
1028 of regression model and observations."
1029 .to_string(),
1030 ));
1031 } else {
1032 return Err(GpError::LikelihoodComputationError(
1034 "ft is too ill conditioned, try another theta again".to_string(),
1035 ));
1036 }
1037 }
1038 let yt = r_chol.solve_triangular(&ytrain.data, UPLO::Lower)?;
1039
1040 let beta = ft_qr_r.solve_triangular_into(ft_qr_q.t().dot(&yt), UPLO::Upper)?;
1041 let rho = yt - ft.dot(&beta);
1042 let rho_sqr = rho.mapv(|v| v * v).sum_axis(Axis(0));
1043
1044 let gamma = r_chol.t().solve_triangular_into(rho, UPLO::Upper)?;
1045 let n_obs: F = F::cast(x_distances.n_obs);
1048
1049 let logdet = r_chol.diag().mapv(|v: F| v.log10()).sum() * F::cast(2.) / n_obs;
1050
1051 let sigma2 = rho_sqr / n_obs;
1053 let reduced_likelihood = -n_obs * (sigma2.sum().log10() + logdet);
1054
1055 Ok((
1056 reduced_likelihood,
1057 GpInnerParams {
1058 sigma2: sigma2[0] * ytrain.std[0] * ytrain.std[0],
1059 beta,
1060 gamma,
1061 r_chol,
1062 ft,
1063 ft_qr_r,
1064 },
1065 ))
1066}
1067
1068#[cfg(feature = "blas")]
1070fn reduced_likelihood<F: Float>(
1071 fx: &ArrayBase<impl Data<Elem = F>, Ix2>,
1072 rxx: ArrayBase<impl Data<Elem = F>, Ix2>,
1073 x_distances: &DiffMatrix<F>,
1074 ytrain: &NormalizedData<F>,
1075 nugget: F,
1076) -> Result<(F, GpInnerParams<F>)> {
1077 let mut r_mx: Array2<F> = Array2::<F>::eye(x_distances.n_obs).mapv(|v| v + v * nugget);
1079 for (i, ij) in x_distances.d_indices.outer_iter().enumerate() {
1080 r_mx[[ij[0], ij[1]]] = rxx[[i, 0]];
1081 r_mx[[ij[1], ij[0]]] = rxx[[i, 0]];
1082 }
1083
1084 let fxl = fx.to_owned().with_lapack();
1085
1086 let r_chol = r_mx.with_lapack().cholesky(UPLO::Lower)?;
1088
1089 let ft = r_chol.solve_triangular(UPLO::Lower, Diag::NonUnit, &fxl)?;
1091 let (ft_qr_q, ft_qr_r) = ft.qr().unwrap();
1092
1093 let (_, sv_qr_r, _) = ft_qr_r.svd(false, false).unwrap();
1095 let cond_ft = sv_qr_r[sv_qr_r.len() - 1] / sv_qr_r[0];
1096 if F::cast(cond_ft) < F::cast(1e-10) {
1097 let (_, sv_f, _) = &fxl.svd(false, false).unwrap();
1098 let cond_fx = sv_f[0] / sv_f[sv_f.len() - 1];
1099 if F::cast(cond_fx) > F::cast(1e15) {
1100 return Err(GpError::LikelihoodComputationError(
1101 "F is too ill conditioned. Poor combination \
1102 of regression model and observations."
1103 .to_string(),
1104 ));
1105 } else {
1106 return Err(GpError::LikelihoodComputationError(
1108 "ft is too ill conditioned, try another theta again".to_string(),
1109 ));
1110 }
1111 }
1112
1113 let yt = r_chol.solve_triangular(
1114 UPLO::Lower,
1115 Diag::NonUnit,
1116 &ytrain.data.to_owned().with_lapack(),
1117 )?;
1118
1119 let beta = ft_qr_r.solve_triangular_into(UPLO::Upper, Diag::NonUnit, ft_qr_q.t().dot(&yt))?;
1120
1121 let rho = yt - ft.dot(&beta);
1122 let rho_sqr = rho.mapv(|v| v * v).sum_axis(Axis(0));
1123 let rho_sqr = rho_sqr.without_lapack();
1124
1125 let gamma = r_chol
1126 .t()
1127 .solve_triangular_into(UPLO::Upper, Diag::NonUnit, rho)?;
1128
1129 let n_obs: F = F::cast(x_distances.n_obs);
1132
1133 let logdet = r_chol
1134 .to_owned()
1135 .without_lapack()
1136 .diag()
1137 .mapv(|v: F| v.log10())
1138 .sum()
1139 * F::cast(2.)
1140 / n_obs;
1141
1142 let sigma2: Array1<F> = rho_sqr / n_obs;
1144 let reduced_likelihood = -n_obs * (sigma2.sum().log10() + logdet);
1145 Ok((
1146 reduced_likelihood,
1147 GpInnerParams {
1148 sigma2: sigma2[0] * ytrain.std[0] * ytrain.std[0],
1149 beta: beta.without_lapack(),
1150 gamma: gamma.without_lapack(),
1151 r_chol: r_chol.without_lapack(),
1152 ft: ft.without_lapack(),
1153 ft_qr_r: ft_qr_r.without_lapack(),
1154 },
1155 ))
1156}
1157
1158pub(crate) fn sample<F: Float>(
1164 x: &ArrayBase<impl Data<Elem = F>, Ix2>,
1165 mean_x: Array2<F>,
1166 cov_x: Array2<F>,
1167 n_traj: usize,
1168 method: GpSamplingMethod,
1169) -> Array2<F> {
1170 let n_eval = x.nrows();
1171 let c = match method {
1172 GpSamplingMethod::Cholesky => {
1173 #[cfg(not(feature = "blas"))]
1174 let c = cov_x.with_lapack().cholesky().unwrap();
1175 #[cfg(feature = "blas")]
1176 let c = cov_x.with_lapack().cholesky(UPLO::Lower).unwrap();
1177 c
1178 }
1179 GpSamplingMethod::EigenValues => {
1180 #[cfg(feature = "blas")]
1181 let (v, w) = cov_x.with_lapack().eigh(UPLO::Lower).unwrap();
1182 #[cfg(not(feature = "blas"))]
1183 let (v, w) = cov_x.with_lapack().eigh_into().unwrap();
1184 let v = v.mapv(F::cast);
1185 let v = v.mapv(|x| {
1186 if x < F::cast(1e-9) {
1188 return F::zero();
1189 }
1190 x.sqrt()
1191 });
1192 let d = Array2::from_diag(&v).with_lapack();
1193 #[cfg(feature = "blas")]
1194 let c = w.dot(&d);
1195 #[cfg(not(feature = "blas"))]
1196 let c = w.dot(&d);
1197 c
1198 }
1199 }
1200 .without_lapack();
1201 let normal = Normal::new(0., 1.).unwrap();
1202 let ary = Array::random((n_eval, n_traj), normal).mapv(|v| F::cast(v));
1203 mean_x.to_owned() + c.dot(&ary)
1204}
1205
1206#[cfg(test)]
1207mod tests {
1208 use super::*;
1209 use approx::{assert_abs_diff_eq, assert_abs_diff_ne};
1210 use argmin_testfunctions::rosenbrock;
1211 use egobox_doe::{Lhs, LhsKind, SamplingMethod};
1212 use linfa::prelude::Predict;
1213 #[cfg(not(feature = "blas"))]
1214 use linfa_linalg::norm::Norm;
1215 use ndarray::{Array, Zip, arr1, arr2, array};
1216 #[cfg(feature = "blas")]
1217 use ndarray_linalg::Norm;
1218 use ndarray_npy::write_npy;
1219 use ndarray_rand::RandomExt;
1220 use ndarray_rand::rand::SeedableRng;
1221 use ndarray_rand::rand_distr::Uniform;
1222 use ndarray_stats::DeviationExt;
1223 use paste::paste;
1224 use rand_xoshiro::Xoshiro256Plus;
1225
1226 #[test]
1227 fn test_constant_function() {
1228 let dim = 3;
1229 let lim = array![[0., 1.]];
1230 let xlimits = lim.broadcast((dim, 2)).unwrap();
1231 let rng = Xoshiro256Plus::seed_from_u64(42);
1232 let nt = 5;
1233 let xt = Lhs::new(&xlimits).with_rng(rng).sample(nt);
1234 let yt = Array::from_vec(vec![3.1; nt]);
1235 let gp = GaussianProcess::<f64, ConstantMean, SquaredExponentialCorr>::params(
1236 ConstantMean::default(),
1237 SquaredExponentialCorr::default(),
1238 )
1239 .theta_init(array![0.1])
1240 .kpls_dim(Some(1))
1241 .fit(&Dataset::new(xt, yt))
1242 .expect("GP fit error");
1243 let rng = Xoshiro256Plus::seed_from_u64(43);
1244 let xtest = Lhs::new(&xlimits).with_rng(rng).sample(nt);
1245 let ytest = gp.predict(&xtest).expect("prediction error");
1246 assert_abs_diff_eq!(Array::from_elem((nt,), 3.1), ytest, epsilon = 1e-6);
1247 }
1248
1249 macro_rules! test_gp {
1250 ($regr:ident, $corr:ident) => {
1251 paste! {
1252
1253 #[test]
1254 fn [<test_gp_ $regr:snake _ $corr:snake >]() {
1255 let xt = array![[0.0], [1.0], [2.0], [3.0], [4.0]];
1256 let xplot = Array::linspace(0., 4., 100).insert_axis(Axis(1));
1257 let yt = array![0.0, 1.0, 1.5, 0.9, 1.0];
1258 let gp = GaussianProcess::<f64, [<$regr Mean>], [<$corr Corr>] >::params(
1259 [<$regr Mean>]::default(),
1260 [<$corr Corr>]::default(),
1261 )
1262 .theta_init(array![0.1])
1263 .fit(&Dataset::new(xt, yt))
1264 .expect("GP fit error");
1265 let yvals = gp
1266 .predict(&arr2(&[[1.0], [3.5]]))
1267 .expect("prediction error");
1268 let expected_y = arr1(&[1.0, 0.9]);
1269 assert_abs_diff_eq!(expected_y, yvals, epsilon = 0.5);
1270
1271 let gpr_vals = gp.predict(&xplot).unwrap();
1272
1273 let yvars = gp
1274 .predict_var(&arr2(&[[1.0], [3.5]]))
1275 .expect("prediction error");
1276 let expected_vars = arr1(&[0., 0.1]);
1277 assert_abs_diff_eq!(expected_vars, yvars, epsilon = 0.5);
1278
1279 let gpr_vars = gp.predict_var(&xplot).unwrap();
1280
1281 let test_dir = "target/tests";
1282 std::fs::create_dir_all(test_dir).ok();
1283
1284 let xplot_file = stringify!([<gp_x_ $regr:snake _ $corr:snake >]);
1285 let file_path = format!("{}/{}.npy", test_dir, xplot_file);
1286 write_npy(file_path, &xplot).expect("x saved");
1287
1288 let gp_vals_file = stringify!([<gp_vals_ $regr:snake _ $corr:snake >]);
1289 let file_path = format!("{}/{}.npy", test_dir, gp_vals_file);
1290 write_npy(file_path, &gpr_vals).expect("gp vals saved");
1291
1292 let gp_vars_file = stringify!([<gp_vars_ $regr:snake _ $corr:snake >]);
1293 let file_path = format!("{}/{}.npy", test_dir, gp_vars_file);
1294 write_npy(file_path, &gpr_vars).expect("gp vars saved");
1295 }
1296 }
1297 };
1298 }
1299
1300 test_gp!(Constant, SquaredExponential);
1301 test_gp!(Constant, AbsoluteExponential);
1302 test_gp!(Constant, Matern32);
1303 test_gp!(Constant, Matern52);
1304
1305 test_gp!(Linear, SquaredExponential);
1306 test_gp!(Linear, AbsoluteExponential);
1307 test_gp!(Linear, Matern32);
1308 test_gp!(Linear, Matern52);
1309
1310 test_gp!(Quadratic, SquaredExponential);
1311 test_gp!(Quadratic, AbsoluteExponential);
1312 test_gp!(Quadratic, Matern32);
1313 test_gp!(Quadratic, Matern52);
1314
1315 fn griewank(x: &Array2<f64>) -> Array1<f64> {
1316 let dim = x.ncols();
1317 let d = Array1::linspace(1., dim as f64, dim).mapv(|v| v.sqrt());
1318 let mut y = Array1::zeros((x.nrows(),));
1319 Zip::from(&mut y).and(x.rows()).for_each(|y, x| {
1320 let s = x.mapv(|v| v * v).sum() / 4000.;
1321 let p = (x.to_owned() / &d)
1322 .mapv(|v| v.cos())
1323 .fold(1., |acc, x| acc * x);
1324 *y = s - p + 1.;
1325 });
1326 y
1327 }
1328
1329 #[test]
1330 fn test_griewank() {
1331 let x = array![[1., 1., 1., 1., 1.], [2., 2., 2., 2., 2.]];
1332 assert_abs_diff_eq!(array![0.72890641, 1.01387135], griewank(&x), epsilon = 1e-8);
1333 }
1334
1335 #[test]
1336 fn test_kpls_griewank() {
1337 let dims = [5]; let nts = [100]; let lim = array![[-600., 600.]];
1340
1341 let test_dir = "target/tests";
1342 std::fs::create_dir_all(test_dir).ok();
1343
1344 (0..dims.len()).for_each(|i| {
1345 let dim = dims[i];
1346 let nt = nts[i];
1347 let xlimits = lim.broadcast((dim, 2)).unwrap();
1348
1349 let prefix = "griewank";
1350 let xfilename = format!("{test_dir}/{prefix}_xt_{nt}x{dim}.npy");
1351 let yfilename = format!("{test_dir}/{prefix}_yt_{nt}x1.npy");
1352
1353 let rng = Xoshiro256Plus::seed_from_u64(42);
1354 let xt = Lhs::new(&xlimits).with_rng(rng).sample(nt);
1355 write_npy(xfilename, &xt).expect("cannot save xt");
1356 let yt = griewank(&xt);
1357 write_npy(yfilename, &yt).expect("cannot save yt");
1358
1359 let gp = GaussianProcess::<f64, ConstantMean, SquaredExponentialCorr>::params(
1360 ConstantMean::default(),
1361 SquaredExponentialCorr::default(),
1362 )
1363 .kpls_dim(Some(3))
1364 .fit(&Dataset::new(xt, yt))
1365 .expect("GP fit error");
1366
1367 let rng = Xoshiro256Plus::seed_from_u64(0);
1368 let xtest = Lhs::new(&xlimits).with_rng(rng).sample(100);
1369 let ytest = gp.predict(&xtest).expect("prediction error");
1371 let ytrue = griewank(&xtest);
1372
1373 let nrmse = (ytrue.to_owned() - &ytest).norm_l2() / ytrue.norm_l2();
1374 println!(
1375 "diff={} ytrue={} nrsme={}",
1376 (ytrue.to_owned() - &ytest).norm_l2(),
1377 ytrue.norm_l2(),
1378 nrmse
1379 );
1380 assert_abs_diff_eq!(nrmse, 0., epsilon = 1e-2);
1381 });
1382 }
1383
1384 fn tensor_product_exp(x: &ArrayBase<impl Data<Elem = f64>, Ix2>) -> Array1<f64> {
1385 x.mapv(|v| v.exp()).map_axis(Axis(1), |row| row.product())
1386 }
1387
1388 #[test]
1389 fn test_kpls_tp_exp() {
1390 let dim = 3;
1391 let nt = 300;
1392 let lim = array![[-1., 1.]];
1393 let xlimits = lim.broadcast((dim, 2)).unwrap();
1394 let rng = Xoshiro256Plus::seed_from_u64(42);
1395 let xt = Lhs::new(&xlimits).with_rng(rng).sample(nt);
1396 let yt = tensor_product_exp(&xt);
1397
1398 let gp = GaussianProcess::<f64, ConstantMean, SquaredExponentialCorr>::params(
1399 ConstantMean::default(),
1400 SquaredExponentialCorr::default(),
1401 )
1402 .kpls_dim(Some(1))
1403 .fit(&Dataset::new(xt, yt))
1404 .expect("GP training");
1405
1406 let xv = Lhs::new(&xlimits).sample(100);
1407 let yv = tensor_product_exp(&xv);
1408
1409 let ytest = gp.predict(&xv).unwrap();
1410 let err = ytest.l2_dist(&yv).unwrap() / yv.norm_l2();
1411 assert_abs_diff_eq!(err, 0., epsilon = 2e-2);
1412 }
1413
1414 fn rosenb(x: &ArrayBase<impl Data<Elem = f64>, Ix2>) -> Array1<f64> {
1415 let mut y: Array1<f64> = Array1::zeros((x.nrows(),));
1416 Zip::from(&mut y).and(x.rows()).par_for_each(|yi, xi| {
1417 *yi = rosenbrock(&xi.to_vec());
1418 });
1419 y
1420 }
1421
1422 #[test]
1423 fn test_kpls_rosenb() {
1424 let dim = 20;
1425 let nt = 30;
1426 let lim = array![[-1., 1.]];
1427 let xlimits = lim.broadcast((dim, 2)).unwrap();
1428 let rng = Xoshiro256Plus::seed_from_u64(42);
1429 let xt = Lhs::new(&xlimits).with_rng(rng).sample(nt);
1430 let yt = rosenb(&xt);
1431
1432 let gp = GaussianProcess::<f64, ConstantMean, Matern32Corr>::params(
1433 ConstantMean::default(),
1434 Matern52Corr::default(),
1435 )
1436 .kpls_dim(Some(1))
1437 .fit(&Dataset::new(xt.to_owned(), yt))
1438 .expect("GP training");
1439
1440 let rng2 = Xoshiro256Plus::seed_from_u64(41);
1441 let xv = Lhs::new(&xlimits).with_rng(rng2).sample(300);
1442 let yv = rosenb(&xv);
1443
1444 let ytest = gp.predict(&xv).expect("GP prediction");
1445 let err = ytest.l2_dist(&yv).unwrap() / yv.norm_l2();
1446 assert_abs_diff_eq!(err, 0., epsilon = 4e-1);
1447
1448 let var = GpVariancePredictor(&gp).predict(&xt);
1449 assert_abs_diff_eq!(var, Array1::zeros(nt), epsilon = 2e-1);
1450 }
1451
1452 fn sphere(x: &Array2<f64>) -> Array1<f64> {
1453 (x * x).sum_axis(Axis(1))
1454 }
1455
1456 fn dsphere(x: &Array2<f64>) -> Array2<f64> {
1457 x.mapv(|v| 2. * v)
1458 }
1459
1460 fn norm1(x: &Array2<f64>) -> Array1<f64> {
1461 x.mapv(|v| v.abs()).sum_axis(Axis(1)).to_owned()
1462 }
1463
1464 fn dnorm1(x: &Array2<f64>) -> Array2<f64> {
1465 x.mapv(|v| if v > 0. { 1. } else { -1. })
1466 }
1467
1468 macro_rules! test_gp_derivatives {
1469 ($regr:ident, $corr:ident, $func:ident, $limit:expr_2021, $nt:expr_2021) => {
1470 paste! {
1471
1472 #[test]
1473 fn [<test_gp_derivatives_ $regr:snake _ $corr:snake>]() {
1474 let mut rng = Xoshiro256Plus::seed_from_u64(42);
1475 let xt = egobox_doe::Lhs::new(&array![[-$limit, $limit], [-$limit, $limit]])
1476 .kind(egobox_doe::LhsKind::CenteredMaximin)
1477 .with_rng(rng.clone())
1478 .sample($nt);
1479
1480 let yt = [<$func>](&xt);
1481 let gp = GaussianProcess::<f64, [<$regr Mean>], [<$corr Corr>] >::params(
1482 [<$regr Mean>]::default(),
1483 [<$corr Corr>]::default(),
1484 )
1485 .fit(&Dataset::new(xt, yt))
1486 .expect("GP fitting");
1487
1488 let x = Array::random_using((2,), Uniform::new(-$limit, $limit), &mut rng);
1489 let xa: f64 = x[0];
1491 let xb: f64 = x[1];
1492 let e = 1e-5;
1493
1494 let x = array![
1495 [xa, xb],
1496 [xa + e, xb],
1497 [xa - e, xb],
1498 [xa, xb + e],
1499 [xa, xb - e]
1500 ];
1501
1502 let y_pred = gp.predict(&x).unwrap();
1503 println!("value at [{},{}] = {}", xa, xb, y_pred);
1504 let y_deriv = gp.predict_gradients(&x);
1505 println!("deriv at [{},{}] = {}", xa, xb, y_deriv);
1506 let true_deriv = [<d $func>](&array![[xa, xb]]);
1507 println!("true deriv at [{},{}] = {}", xa, xb, true_deriv);
1508 println!("jacob = at [{},{}] = {}", xa, xb, gp.predict_jacobian(&array![xa, xb]));
1509
1510 let diff_g = (y_pred[1] - y_pred[2]) / (2. * e);
1511 let diff_d = (y_pred[3] - y_pred[4]) / (2. * e);
1512
1513 if (diff_g-true_deriv[[0, 0]]).abs() < 10. {
1515 assert_rel_or_abs_error(y_deriv[[0, 0]], diff_g);
1516 }
1517 if (diff_d-true_deriv[[0, 1]]).abs() < 10. {
1518 assert_rel_or_abs_error(y_deriv[[0, 1]], diff_d);
1519 }
1520 }
1521 }
1522 };
1523 }
1524
1525 test_gp_derivatives!(Constant, SquaredExponential, sphere, 10., 10);
1526 test_gp_derivatives!(Linear, SquaredExponential, sphere, 10., 10);
1527 test_gp_derivatives!(Quadratic, SquaredExponential, sphere, 10., 10);
1528 test_gp_derivatives!(Constant, AbsoluteExponential, sphere, 10., 10);
1529 test_gp_derivatives!(Linear, AbsoluteExponential, norm1, 10., 16);
1530 test_gp_derivatives!(Quadratic, AbsoluteExponential, norm1, 10., 16);
1531 test_gp_derivatives!(Constant, Matern32, norm1, 10., 16);
1532 test_gp_derivatives!(Linear, Matern32, norm1, 10., 16);
1533 test_gp_derivatives!(Quadratic, Matern32, sphere, 10., 16);
1534 test_gp_derivatives!(Constant, Matern52, norm1, 10., 16);
1535 test_gp_derivatives!(Linear, Matern52, norm1, 10., 16);
1536 test_gp_derivatives!(Quadratic, Matern52, sphere, 10., 10);
1537
1538 #[allow(unused_macros)]
1539 macro_rules! test_gp_variance_derivatives {
1540 ($regr:ident, $corr:ident, $func:ident, $limit:expr_2021, $nt:expr_2021) => {
1541 paste! {
1542
1543 #[test]
1544 fn [<test_gp_variance_derivatives_ $regr:snake _ $corr:snake _ $func:snake>]() {
1545 let mut rng = Xoshiro256Plus::seed_from_u64(42);
1546 let xt = egobox_doe::Lhs::new(&array![[-$limit, $limit], [-$limit, $limit]]).with_rng(rng.clone()).sample($nt);
1547 let yt = [<$func>](&xt);
1548 println!(stringify!(<$func>));
1549
1550 let gp = GaussianProcess::<f64, [<$regr Mean>], [<$corr Corr>] >::params(
1551 [<$regr Mean>]::default(),
1552 [<$corr Corr>]::default(),
1553 )
1554 .fit(&Dataset::new(xt, yt))
1555 .expect("GP fitting");
1556
1557 for _ in 0..10 {
1558 let x = Array::random_using((2,), Uniform::new(-$limit, $limit), &mut rng);
1559 let xa: f64 = x[0];
1560 let xb: f64 = x[1];
1561 let e = 1e-5;
1562
1563 let x = array![
1564 [xa, xb],
1565 [xa + e, xb],
1566 [xa - e, xb],
1567 [xa, xb + e],
1568 [xa, xb - e]
1569 ];
1570 println!("****************************************");
1571 let y_pred = gp.predict(&x).unwrap();
1572 println!("value at [{},{}] = {}", xa, xb, y_pred);
1573 let y_deriv = gp.predict_gradients(&x);
1574 println!("deriv at [{},{}] = {}", xa, xb, y_deriv);
1575 let y_pred = gp.predict_var(&x).unwrap();
1576 println!("variance at [{},{}] = {}", xa, xb, y_pred);
1577 let y_deriv = gp.predict_var_gradients(&x);
1578 println!("variance deriv at [{},{}] = {}", xa, xb, y_deriv);
1579
1580 let diff_g = (y_pred[1] - y_pred[2]) / (2. * e);
1581 let diff_d = (y_pred[3] - y_pred[4]) / (2. * e);
1582
1583 assert_rel_or_abs_error(y_deriv[[0, 0]], diff_g);
1584 assert_rel_or_abs_error(y_deriv[[0, 1]], diff_d);
1585 }
1586 }
1587 }
1588 };
1589 }
1590
1591 test_gp_variance_derivatives!(Constant, SquaredExponential, sphere, 10., 100);
1592 test_gp_variance_derivatives!(Linear, SquaredExponential, sphere, 10., 100);
1593 test_gp_variance_derivatives!(Quadratic, SquaredExponential, sphere, 10., 100);
1594 #[cfg(not(feature = "nlopt"))]
1596 test_gp_variance_derivatives!(Constant, AbsoluteExponential, norm1, 10., 100);
1597 test_gp_variance_derivatives!(Linear, AbsoluteExponential, norm1, 1., 50);
1598 test_gp_variance_derivatives!(Quadratic, AbsoluteExponential, sphere, 10., 100);
1599 test_gp_variance_derivatives!(Constant, Matern32, sphere, 10., 100);
1600 test_gp_variance_derivatives!(Linear, Matern32, norm1, 1., 50);
1601 test_gp_variance_derivatives!(Quadratic, Matern32, sphere, 10., 100);
1602 test_gp_variance_derivatives!(Constant, Matern52, sphere, 10., 100);
1603 test_gp_variance_derivatives!(Linear, Matern52, norm1, 1., 50);
1604 test_gp_variance_derivatives!(Quadratic, Matern52, sphere, 10., 100);
1605
1606 #[test]
1607 fn test_variance_derivatives() {
1608 let xt = egobox_doe::FullFactorial::new(&array![[-10., 10.], [-10., 10.]]).sample(10);
1609 let yt = sphere(&xt);
1610
1611 let gp = GaussianProcess::<f64, ConstantMean, SquaredExponentialCorr>::params(
1612 ConstantMean::default(),
1613 SquaredExponentialCorr::default(),
1614 )
1615 .fit(&Dataset::new(xt, yt))
1616 .expect("GP fitting");
1617
1618 for _ in 0..20 {
1619 let mut rng = Xoshiro256Plus::seed_from_u64(42);
1620 let x = Array::random_using((2,), Uniform::new(-10., 10.), &mut rng);
1621 let xa: f64 = x[0];
1622 let xb: f64 = x[1];
1623 let e = 1e-5;
1624
1625 let x = array![
1626 [xa, xb],
1627 [xa + e, xb],
1628 [xa - e, xb],
1629 [xa, xb + e],
1630 [xa, xb - e]
1631 ];
1632 let y_pred = gp.predict_var(&x).unwrap();
1633 println!("variance at [{xa},{xb}] = {y_pred}");
1634 let y_deriv = gp.predict_var_gradients(&x);
1635 println!("variance deriv at [{xa},{xb}] = {y_deriv}");
1636
1637 let diff_g = (y_pred[1] - y_pred[2]) / (2. * e);
1638 let diff_d = (y_pred[3] - y_pred[4]) / (2. * e);
1639
1640 if y_pred[0].abs() > 1e-1 && y_pred[0].abs() > 1e-1 {
1641 assert_rel_or_abs_error(y_deriv[[0, 0]], diff_g);
1643 }
1644 if y_pred[0].abs() > 1e-1 && y_pred[0].abs() > 1e-1 {
1645 assert_rel_or_abs_error(y_deriv[[0, 1]], diff_d);
1647 }
1648 }
1649 }
1650
1651 #[test]
1652 fn test_fixed_theta() {
1653 let xt = array![[0.0], [1.0], [2.0], [3.0], [4.0]];
1654 let yt = array![0.0, 1.0, 1.5, 0.9, 1.0];
1655 let gp = Kriging::params()
1656 .fit(&Dataset::new(xt.clone(), yt.clone()))
1657 .expect("GP fit error");
1658 let default = ThetaTuning::default();
1659 assert_abs_diff_ne!(*gp.theta(), default.init());
1660 let expected = gp.theta();
1661
1662 let gp = Kriging::params()
1663 .theta_tuning(ThetaTuning::Fixed(expected.clone()))
1664 .fit(&Dataset::new(xt, yt))
1665 .expect("GP fit error");
1666 assert_abs_diff_eq!(*gp.theta(), expected);
1667 }
1668
1669 fn x2sinx(x: &Array2<f64>) -> Array1<f64> {
1670 ((x * x) * (x).mapv(|v| v.sin())).remove_axis(Axis(1))
1671 }
1672
1673 #[test]
1674 fn test_sampling() {
1675 let xdoe = array![[-8.5], [-4.0], [-3.0], [-1.0], [4.0], [7.5]];
1676 let ydoe = x2sinx(&xdoe);
1677 let krg = Kriging::<f64>::params()
1678 .fit(&Dataset::new(xdoe, ydoe))
1679 .expect("Kriging training");
1680 let n_plot = 35;
1681 let n_traj = 10;
1682 let (x_min, x_max) = (-10., 10.);
1683 let x = Array::linspace(x_min, x_max, n_plot)
1684 .into_shape_with_order((n_plot, 1))
1685 .unwrap();
1686 let trajs = krg.sample(&x, n_traj);
1687 assert_eq!(&[n_plot, n_traj], trajs.shape())
1688 }
1689
1690 #[test]
1691 fn test_sampling_eigen() {
1692 let xdoe = array![[-8.5], [-4.0], [-3.0], [-1.0], [4.0], [7.5]];
1693 let ydoe = x2sinx(&xdoe);
1694 let krg = Kriging::<f64>::params()
1695 .fit(&Dataset::new(xdoe, ydoe))
1696 .expect("Kriging training");
1697 let n_plot = 500;
1698 let n_traj = 10;
1699 let (x_min, x_max) = (-10., 10.);
1700 let x = Array::linspace(x_min, x_max, n_plot)
1701 .into_shape_with_order((n_plot, 1))
1702 .unwrap();
1703 let trajs = krg.sample_eig(&x, n_traj);
1704 assert_eq!(&[n_plot, n_traj], trajs.shape());
1705 assert!(!trajs.fold(false, |acc, v| acc || v.is_nan())); }
1707
1708 fn assert_rel_or_abs_error(y_deriv: f64, fdiff: f64) {
1709 println!("analytic deriv = {y_deriv}, fdiff = {fdiff}");
1710 if fdiff.abs() < 1. {
1711 let atol = 1.;
1712 println!("Check absolute error: abs({y_deriv}) should be < {atol}");
1713 assert_abs_diff_eq!(y_deriv, 0.0, epsilon = atol); } else {
1715 let rtol = 6e-1;
1716 let rel_error = (y_deriv - fdiff).abs() / fdiff.abs(); println!("Check relative error: {rel_error} should be < {rtol}");
1718 assert_abs_diff_eq!(rel_error, 0.0, epsilon = rtol);
1719 }
1720 }
1721
1722 fn sin_linear(x: &Array2<f64>) -> Array2<f64> {
1723 let x1 = x.column(0).to_owned().mapv(|v| v.sin());
1725 let x2 = x.column(0).mapv(|v| 2. * v) + x.column(1).mapv(|v| 5. * v);
1726 (x1 + x2)
1727 .mapv(|v| v + 10.)
1728 .into_shape_with_order((x.nrows(), 1))
1729 .unwrap()
1730 }
1731
1732 #[test]
1733 fn test_bug_var_derivatives() {
1734 let _xt = egobox_doe::Lhs::new(&array![[-5., 10.], [-5., 10.]])
1735 .kind(LhsKind::Centered)
1736 .sample(12);
1737 let _yt = sin_linear(&_xt);
1738
1739 let xt = array![
1740 [6.875, -4.375],
1741 [-3.125, 1.875],
1742 [1.875, -1.875],
1743 [-4.375, 3.125],
1744 [8.125, 9.375],
1745 [4.375, 4.375],
1746 [0.625, 0.625],
1747 [9.375, 6.875],
1748 [5.625, 8.125],
1749 [-0.625, -3.125],
1750 [3.125, 5.625],
1751 [-1.875, -0.625]
1752 ];
1753 let yt = array![
1754 2.43286801,
1755 13.10840811,
1756 5.32908578,
1757 17.81862219,
1758 74.08849877,
1759 39.68137781,
1760 14.96009727,
1761 63.17475741,
1762 61.26331775,
1763 -7.46009727,
1764 44.39159189,
1765 2.17091422,
1766 ];
1767
1768 let gp = GaussianProcess::<f64, ConstantMean, SquaredExponentialCorr>::params(
1769 ConstantMean::default(),
1770 SquaredExponentialCorr::default(),
1771 )
1772 .theta_tuning(ThetaTuning::Fixed(array![
1773 f64::sqrt(2. * 0.0437386),
1774 f64::sqrt(2. * 0.00697978)
1775 ]))
1776 .fit(&Dataset::new(xt, yt))
1777 .expect("GP fitting");
1778
1779 let e = 5e-6;
1780 let xa = -1.3;
1781 let xb = 2.5;
1782 let x = array![
1783 [xa, xb],
1784 [xa + e, xb],
1785 [xa - e, xb],
1786 [xa, xb + e],
1787 [xa, xb - e]
1788 ];
1789 let y_pred = gp.predict_var(&x).unwrap();
1790 let y_deriv = gp.predict_var_gradients(&array![[xa, xb]]);
1791 let diff_g = (y_pred[1] - y_pred[2]) / (2. * e);
1792 let diff_d = (y_pred[3] - y_pred[4]) / (2. * e);
1793
1794 assert_abs_diff_eq!(y_deriv[[0, 0]], diff_g, epsilon = 1e-5);
1795 assert_abs_diff_eq!(y_deriv[[0, 1]], diff_d, epsilon = 1e-5);
1796 }
1797}