1414from spotpython .utils .linalg import matrix_inversion_dispatcher
1515from numpy .linalg import det
1616from spotpython .utils .aggregate import select_distant_points
17+ from sklearn .base import BaseEstimator , RegressorMixin
1718
1819
1920def crude_reset (theta , tmin , tmax , m ):
@@ -114,12 +115,11 @@ def darg(d, X: np.ndarray = None, samp_size: int = 1000) -> dict:
114115 dict: Updated 'd' with fields 'start', 'min', 'max', 'mle', 'ab', etc.
115116
116117 Examples:
117- >>> from spotpython.gp.gp_sep import GPsep
118+ >>> from spotpython.gp.gp_sep import darg
118119 >>> import numpy as np
119120 >>> X = np.array([[1, 2], [3, 4], [5, 6]])
120- >>> gp = GPsep(m=2, n=3, X=X)
121121 >>> d = 2.5
122- >>> result = gp. darg(d=d, X=X, samp_size=10)
122+ >>> result = darg(d=d, X=X, samp_size=10)
123123 >>> print(result)
124124 """
125125 if X is None :
@@ -278,7 +278,7 @@ def garg(g, y: np.ndarray = None) -> dict:
278278 return g
279279
280280
281- class GPsep :
281+ class GPsep ( BaseEstimator , RegressorMixin ) :
282282 """A class to represent a Gaussian Process with separable covariance.
283283
284284 Attributes:
@@ -303,22 +303,22 @@ class GPsep:
303303 verbosity: Verbosity level.
304304 auto_optimize: Whether to automatically optimize hyperparameters.
305305 max_points: Maximum number of points for model building.
306+ seed: Random seed for reproducibility.
306307 """
307308
308309 def __init__ (
309310 self ,
310- X : np .ndarray = None ,
311- y : np .ndarray = None ,
312- d : np .ndarray = None ,
313- g : float = None ,
311+ d = None ,
312+ g = None ,
314313 nlsep_method = "inv" ,
315314 gradnlsep_method = "inv" ,
316315 n_restarts_optimizer = 9 ,
317- samp_size : int = 1000 ,
316+ samp_size = 1000 ,
318317 maxit = 100 ,
319318 verbosity = 0 ,
320319 auto_optimize = True ,
321320 max_points = None ,
321+ seed = 123 ,
322322 ) -> None :
323323 """
324324 Initialize the GP model with data and hyperparameters.
@@ -349,31 +349,9 @@ def __init__(
349349 max_points (int):
350350 Maximum number of points to use for the model building. Default is None, which means all points are used.
351351 """
352- if X is not None :
353- # convert pandas dataframes or series to numpy arrays
354- if hasattr (X , "to_numpy" ):
355- X = X .to_numpy ()
356- if y is not None :
357- if hasattr (y , "to_numpy" ):
358- y = y .to_numpy ()
359- y = y .reshape (- 1 , 1 )
360- if X is not None and y is not None :
361- if max_points is not None :
362- X , y = select_distant_points (X , y , max_points )
363- print (f"Selected { max_points } points for the model." )
364- self .m = None # (int) number of input dimensions
365- self .n = None # (int) number of observations
366- self .X = X
367- self .y = y
352+ # Hyperparameters (do not store training data)
368353 self .d = d
369354 self .g = g
370- self .K = None
371- self .Ki = None
372- self .Kiy = None
373- self .phi = None
374- self .dK = None # boolean
375- self .DK = None # matrix
376- self .ldetK = None
377355 self .nlsep_method = nlsep_method
378356 self .gradnlsep_method = gradnlsep_method
379357 self .n_restarts_optimizer = n_restarts_optimizer
@@ -382,11 +360,27 @@ def __init__(
382360 self .verbosity = verbosity
383361 self .auto_optimize = auto_optimize
384362 self .max_points = max_points
363+ self .seed = seed
364+
365+ # Attributes set during fit
366+ self .m = None
367+ self .n = None
368+ self .X_ = None
369+ self .y_ = None
370+ self .dk = None # derivative flag
371+ self .K = None
372+ self .Ki = None
373+ self .Kiy = None
374+ self .phi = None
375+ self .dK = None
376+ self .DK = None
377+ self .ldetK = None
378+
379+ # Internal flag to check if fitted
380+ self ._is_fitted = False
385381
386382 # need to store the initial parameters for the fit method (sklearn compatibility)
387383 self .init_params = {
388- "X" : X ,
389- "y" : y ,
390384 "d" : d ,
391385 "g" : g ,
392386 "nlsep_method" : nlsep_method ,
@@ -397,6 +391,7 @@ def __init__(
397391 "verbosity" : verbosity ,
398392 "auto_optimize" : auto_optimize ,
399393 "max_points" : max_points ,
394+ "seed" : seed ,
400395 }
401396
402397 # Add these two methods required by scikit-learn
@@ -413,8 +408,6 @@ def get_params(self, deep=True):
413408 dict: Parameter names mapped to their values.
414409 """
415410 return {
416- "X" : self .X ,
417- "y" : self .y ,
418411 "d" : self .d ,
419412 "g" : self .g ,
420413 "nlsep_method" : self .nlsep_method ,
@@ -425,6 +418,7 @@ def get_params(self, deep=True):
425418 "verbosity" : self .verbosity ,
426419 "auto_optimize" : self .auto_optimize ,
427420 "max_points" : self .max_points ,
421+ "seed" : self .seed ,
428422 }
429423
430424 def set_params (self , ** parameters ):
@@ -450,8 +444,8 @@ def fit(self, X: np.ndarray, y: np.ndarray, d=None, g=None, dK: bool = True, aut
450444 """Fit the GP model with training data and optionally auto-optimize hyperparameters.
451445
452446 Args:
453- X: The input data matrix of shape (n, m).
454- y: The output data vector of length n.
447+ X: array-like of shape (n_samples, n_features)
448+ y: array-like of shape (n_samples,)
455449 d: The length-scale parameters. If None, will be determined
456450 automatically. Defaults to None.
457451 g: The nugget parameter. If None, will be determined automatically.
@@ -475,10 +469,12 @@ def fit(self, X: np.ndarray, y: np.ndarray, d=None, g=None, dK: bool = True, aut
475469 if hasattr (y , "to_numpy" ):
476470 y = y .to_numpy ()
477471 y = y .reshape (- 1 , 1 )
478- print (f"X shape: { X .shape } , y shape: { y .shape } " )
472+ if verbosity > 0 :
473+ print (f"X shape: { X .shape } , y shape: { y .shape } " )
479474 if self .max_points is not None :
480475 X , y = select_distant_points (X , y , self .max_points )
481- print (f"Selected { self .max_points } points for the model." )
476+ if verbosity > 0 :
477+ print (f"Selected { self .max_points } points for the model." )
482478 if auto_optimize is None :
483479 auto_optimize = self .auto_optimize
484480 n , m = X .shape
@@ -600,7 +596,9 @@ def objective(par):
600596 def gradient (par ):
601597 return gradnlsep (par , X , y , self .gradnlsep_method )
602598
603- result = run_minimize_with_restarts (objective = objective , gradient = gradient , x0 = p , bounds = bounds , n_restarts_optimizer = self .n_restarts_optimizer , maxit = self .maxit , verb = self .verbosity )
599+ result = run_minimize_with_restarts (
600+ objective = objective , gradient = gradient , x0 = p , bounds = bounds , n_restarts_optimizer = self .n_restarts_optimizer , maxit = self .maxit , verb = self .verbosity , random_state = self .seed
601+ )
604602
605603 d = result .x [:- 1 ]
606604 g = result .x [- 1 ]
@@ -611,7 +609,7 @@ def gradient(par):
611609 print (f"result: { result } " )
612610 print (f"Optimized d: { d } , g: { g } " )
613611 print (f"Updated d: { self .d } , g: { self .g } " )
614- self .build ()
612+ self ._build ()
615613 new_theta = np .concatenate ((self .get_d (), [self .get_g ()]))
616614 if np .sqrt (np .mean ((result .x - new_theta ) ** 2 )) > np .sqrt (np .finfo (float ).eps ):
617615 warnings .warn ("stored theta not the same as theta-hat" , RuntimeWarning )
@@ -622,10 +620,12 @@ def gradient(par):
622620 print (f"Optimized nugget (g): { self .get_g ()} " )
623621 print (f"Message: { result ['msg' ]} " )
624622 print (f"Iterations: { result ['its' ]} " )
623+ self ._is_fitted = True
625624 return self
626625 else :
627626 # No optimization, just build the model with roughly estimated parameters using darg and garg
628- self .build ()
627+ self ._build ()
628+ self ._is_fitted = True
629629 return self
630630 else :
631631 # Original behavior for explicitly provided parameters
@@ -634,7 +634,8 @@ def gradient(par):
634634 if len (self .d ) != m :
635635 raise ValueError (f"Length of d ({ len (self .d )} ) does not match ncol(X) ({ m } )" )
636636 self .g = g
637- self .build ()
637+ self ._build ()
638+ self ._is_fitted = True
638639 return self
639640
640641 def calc_ytKiy (self ) -> None :
@@ -656,7 +657,7 @@ def calc_ytKiy(self) -> None:
656657 self .phi = phi [0 , 0 ]
657658 self .Kiy = Kiy
658659
659- def build (self ) -> None :
660+ def _build (self ) -> None :
660661 """
661662 Completes all correlation calculations after data is defined.
662663 """
@@ -674,11 +675,15 @@ def build(self) -> None:
674675 # # raise RuntimeError("dK calculations have already been initialized.")
675676 # self.DK = diff_covar_sep_symm(self.m, self.X, self.n, self.d, self.K)
676677
677- def predict (self , XX : np .ndarray , lite : bool = False , nonug : bool = False , return_full = False , return_std = False ) -> float :
678+ def _check_is_fitted (self ):
679+ if not self ._is_fitted :
680+ raise ValueError ("This GPsep instance is not fitted yet. Call 'fit' with " "appropriate arguments before using 'predict'." )
681+
682+ def predict (self , X : np .ndarray , lite : bool = False , nonug : bool = False , return_full = False , return_std = False ) -> float :
678683 """Predict the Gaussian Process output at new input points.
679684
680685 Args:
681- XX : The predictive locations.
686+ X : The predictive locations.
682687 lite: Flag to indicate whether to compute only the diagonal
683688 of Sigma. Defaults to False.
684689 nonug: Flag to indicate whether to exclude nugget.
@@ -724,54 +729,55 @@ def predict(self, XX: np.ndarray, lite: bool = False, nonug: bool = False, retur
724729 plt.title("Predictive Distribution")
725730 plt.show()
726731 """
727- # if XX is a pandas dataframe, convert it to a numpy array
728- if hasattr (XX , "to_numpy" ):
729- XX = XX .to_numpy ()
732+ self ._check_is_fitted ()
733+ # if X is a pandas dataframe, convert it to a numpy array
734+ if hasattr (X , "to_numpy" ):
735+ X = X .to_numpy ()
730736 if lite :
731- res = self ._predict_lite (XX , nonug )
737+ res = self ._predict_lite (X , nonug )
732738 if return_full :
733739 return res
734740 elif return_std :
735741 return (res ["mean" ], res ["s2" ])
736742 else :
737743 return res ["mean" ]
738744 else :
739- res = self ._predict_full (XX , nonug )
745+ res = self ._predict_full (X , nonug )
740746 if return_full :
741747 return res
742748 elif return_std :
743749 return (res ["mean" ], res ["Sigma" ])
744750 else :
745751 return res ["mean" ]
746752
747- def _predict_lite (self , XX : np .ndarray , nonug : bool ) -> dict :
753+ def _predict_lite (self , X : np .ndarray , nonug : bool ) -> dict :
748754 """
749755 Predict only the diagonal of Sigma—optimized for speed.
750756
751757 Args:
752- XX (np.ndarray): The predictive locations.
758+ X (np.ndarray): The predictive locations.
753759 nonug (bool): Flag to indicate whether to use nugget.
754760
755761 Returns:
756762 dict: A dictionary containing the mean, s2, df, and llik.
757763 """
758- nn = XX .shape [0 ]
759- m = XX .shape [1 ]
760- mean_out , s2_out , df_out , llik_out = predGPsep_lite (self , m , nn , XX , lite_in = True , nonug_in = nonug )
764+ nn = X .shape [0 ]
765+ m = X .shape [1 ]
766+ mean_out , s2_out , df_out , llik_out = predGPsep_lite (self , m , nn , X , lite_in = True , nonug_in = nonug )
761767 return {"mean" : mean_out , "s2" : s2_out , "df" : df_out , "llik" : llik_out }
762768
763- def _predict_full (self , XX : np .ndarray , nonug : bool ) -> dict :
769+ def _predict_full (self , X : np .ndarray , nonug : bool ) -> dict :
764770 """
765771 Compute full predictive covariance matrix.
766772
767773 Args:
768- XX (np.ndarray): The predictive locations.
774+ X (np.ndarray): The predictive locations.
769775 nonug (bool): Flag to indicate whether to use nugget.
770776
771777 Returns:
772778 dict: A dictionary containing the mean, Sigma, df, and llik.
773779 """
774- nn , m = XX .shape
780+ nn , m = X .shape
775781 if m != self .m :
776782 raise ValueError (f"ncol(X)={ m } does not match GPsep model ({ self .m } )" )
777783
@@ -785,8 +791,8 @@ def _predict_full(self, XX: np.ndarray, nonug: bool) -> dict:
785791 df [0 ] = float (n )
786792 phidf = self .phi / df [0 ]
787793 llik [0 ] = - 0.5 * (df [0 ] * np .log (0.5 * self .phi ) + self .ldetK )
788- k = covar_sep (self .m , self .X , n , XX , nn , self .d , 0.0 )
789- Sigma [...] = covar_sep_symm (self .m , XX , nn , self .d , g )
794+ k = covar_sep (self .m , self .X , n , X , nn , self .d , 0.0 )
795+ Sigma [...] = covar_sep_symm (self .m , X , nn , self .d , g )
790796 ktKi = np .dot (k .T , self .Ki )
791797 mean [:] = np .dot (ktKi , self .y ).reshape (- 1 )
792798 Sigma [...] = phidf * (Sigma - np .dot (ktKi , k ))
@@ -880,7 +886,7 @@ def gradient(par):
880886 # set new parameters and build
881887 self .set_new_params (d , g )
882888 print (f"Updated d: { self .d } , g: { self .g } " )
883- self .build ()
889+ self ._build ()
884890 return {"parameters" : result .x , "iterations" : result .nit , "convergence" : result .status , "message" : result .message }
885891
886892
0 commit comments