Coverage for src/cosmic_toolbox/TransformedGaussianMixture.py: 97%

74 statements  

« prev     ^ index     » next       coverage.py v7.15.0, created at 2026-07-10 10:31 +0000

1""" 

2Transformed Gaussian Mixture Model. 

3 

4Provides a Gaussian Mixture Model that operates in a transformed parameter space 

5to handle bounded parameters more effectively. 

6""" 

7 

8import numpy as np 

9from scipy.stats import norm 

10from sklearn.mixture import GaussianMixture 

11 

12 

13def scale_fwd(x, param_bounds, param_bounds_trans): 

14 """ 

15 Scale values from original bounds to transformed bounds. 

16 

17 :param x: Values to scale. 

18 :type x: numpy.ndarray 

19 :param param_bounds: Original parameter bounds [min, max]. 

20 :type param_bounds: array-like 

21 :param param_bounds_trans: Transformed parameter bounds [min, max]. 

22 :type param_bounds_trans: array-like 

23 :return: Scaled values. 

24 :rtype: numpy.ndarray 

25 """ 

26 xs = x - param_bounds[0] 

27 xs = ( 

28 xs 

29 / (param_bounds[1] - param_bounds[0]) 

30 * (param_bounds_trans[1] - param_bounds_trans[0]) 

31 ) 

32 xs = xs + param_bounds_trans[0] 

33 return xs 

34 

35 

36def scale_inv(x, param_bounds, param_bounds_trans): 

37 """ 

38 Scale values from transformed bounds back to original bounds. 

39 

40 :param x: Values to scale. 

41 :type x: numpy.ndarray 

42 :param param_bounds: Original parameter bounds [min, max]. 

43 :type param_bounds: array-like 

44 :param param_bounds_trans: Transformed parameter bounds [min, max]. 

45 :type param_bounds_trans: array-like 

46 :return: Scaled values. 

47 :rtype: numpy.ndarray 

48 """ 

49 xs = x - param_bounds_trans[0] 

50 xs = ( 

51 xs 

52 * (param_bounds[1] - param_bounds[0]) 

53 / (param_bounds_trans[1] - param_bounds_trans[0]) 

54 ) 

55 xs = xs + param_bounds[0] 

56 return xs 

57 

58 

59def trans_fwd(x, param_bounds, param_bounds_trans): 

60 """ 

61 Transform values forward using normal PPF (probit transform). 

62 

63 :param x: Values to transform. 

64 :type x: numpy.ndarray 

65 :param param_bounds: Original parameter bounds [min, max]. 

66 :type param_bounds: array-like 

67 :param param_bounds_trans: Transformed parameter bounds [min, max]. 

68 :type param_bounds_trans: array-like 

69 :return: Transformed values. 

70 :rtype: numpy.ndarray 

71 """ 

72 xs = scale_fwd(x, param_bounds, param_bounds_trans) 

73 ppfx = norm.ppf(xs) 

74 if np.any(~np.isfinite(ppfx)): 

75 import ipdb 

76 

77 ipdb.set_trace() 

78 return ppfx 

79 

80 

81def trans_inv(x, param_bounds, param_bounds_trans): 

82 """ 

83 Transform values back using normal CDF (inverse probit transform). 

84 

85 :param x: Values to transform. 

86 :type x: numpy.ndarray 

87 :param param_bounds: Original parameter bounds [min, max]. 

88 :type param_bounds: array-like 

89 :param param_bounds_trans: Transformed parameter bounds [min, max]. 

90 :type param_bounds_trans: array-like 

91 :return: Inverse-transformed values. 

92 :rtype: numpy.ndarray 

93 """ 

94 xi = norm.cdf(x) 

95 xi = scale_inv(xi, param_bounds, param_bounds_trans) 

96 return xi 

97 

98 

99class TransformedGaussianMixture: 

100 """ 

101 Gaussian Mixture Model with parameter transformation. 

102 

103 This class wraps sklearn's GaussianMixture to work with bounded parameters 

104 by transforming them to an unbounded space using a probit (normal CDF/PPF) 

105 transformation. 

106 

107 :param param_bounds: Parameter bounds for each dimension, shape (n_dims, 2). 

108 If None, bounds are inferred from the data during fit. 

109 :type param_bounds: numpy.ndarray or None 

110 :param args: Positional arguments passed to GaussianMixture. 

111 :param kwargs: Keyword arguments passed to GaussianMixture. 

112 """ 

113 

114 def __init__(self, param_bounds=None, *args, **kwargs): 

115 self.eps = 1e-8 

116 self.bounds_trans = [self.eps, 1 - self.eps] 

117 self.param_bounds = param_bounds 

118 

119 self.gm = GaussianMixture(*args, **kwargs) 

120 

121 def set_bounds(self, X): 

122 """ 

123 Set parameter bounds from data if not already set. 

124 

125 :param X: Training data, shape (n_samples, n_features). 

126 :type X: numpy.ndarray 

127 """ 

128 if self.param_bounds is None: 

129 self.param_bounds = np.array( 

130 [ 

131 np.min(X, axis=0) - 10 * self.eps, 

132 np.max(X, axis=0) + 10 * self.eps, 

133 ] 

134 ).T 

135 

136 def fit(self, X, y=None): 

137 """ 

138 Fit the Gaussian Mixture Model. 

139 

140 :param X: Training data, shape (n_samples, n_features). 

141 :type X: numpy.ndarray 

142 :param y: Ignored (for sklearn compatibility). 

143 :return: self 

144 """ 

145 self.set_bounds(X) 

146 X_trans = self._transform_params_forward(X) 

147 return self.gm.fit(X_trans, y) 

148 

149 def fit_predict(self, X, y=None): 

150 """ 

151 Fit and predict cluster labels. 

152 

153 :param X: Training data, shape (n_samples, n_features). 

154 :type X: numpy.ndarray 

155 :param y: Ignored (for sklearn compatibility). 

156 :return: Component labels for each sample. 

157 :rtype: numpy.ndarray 

158 """ 

159 self.set_bounds(X) 

160 X_trans = self._transform_params_forward(X) 

161 return self.gm.fit_predict(X_trans, y) 

162 

163 def predict_proba(self, X): 

164 """ 

165 Predict posterior probability of each component given the data. 

166 

167 :param X: Data points, shape (n_samples, n_features). 

168 :type X: numpy.ndarray 

169 :return: Posterior probabilities, shape (n_samples, n_components). 

170 :rtype: numpy.ndarray 

171 """ 

172 X_trans = self._transform_params_forward(X) 

173 return self.gm.predict_proba(X_trans) 

174 

175 def sample(self, n_samples=1): 

176 """ 

177 Generate random samples from the fitted Gaussian mixture. 

178 

179 :param n_samples: Number of samples to generate. 

180 :type n_samples: int 

181 :return: Tuple of (samples, component_labels). 

182 :rtype: tuple(numpy.ndarray, numpy.ndarray) 

183 """ 

184 X_trans, y = self.gm.sample(n_samples) 

185 X = self._transform_params_inverse(X_trans) 

186 return X, y 

187 

188 def score(self, X, y=None): 

189 """ 

190 Compute the per-sample average log-likelihood. 

191 

192 :param X: Data points, shape (n_samples, n_features). 

193 :type X: numpy.ndarray 

194 :param y: Ignored (for sklearn compatibility). 

195 :return: Log-likelihood of the data. 

196 :rtype: float 

197 """ 

198 X_trans = self._transform_params_forward(X) 

199 return self.gm.score(X_trans) 

200 

201 def score_samples(self, X): 

202 """ 

203 Compute the log-likelihood of each sample. 

204 

205 :param X: Data points, shape (n_samples, n_features). 

206 :type X: numpy.ndarray 

207 :return: Log-likelihood for each sample. 

208 :rtype: numpy.ndarray 

209 """ 

210 X_trans = self._transform_params_forward(X) 

211 return self.gm.score_samples(X_trans) 

212 

213 def set_params(self, **params): 

214 """ 

215 Set parameters of the underlying GaussianMixture. 

216 

217 :param params: Parameters to set. 

218 :return: self 

219 """ 

220 return self.gm.set_params(**params) 

221 

222 def get_params(self, deep=True): 

223 """ 

224 Get parameters of the underlying GaussianMixture. 

225 

226 :param deep: If True, return parameters for sub-objects. 

227 :type deep: bool 

228 :return: Parameter names mapped to their values. 

229 :rtype: dict 

230 """ 

231 return self.gm.get_params(deep) 

232 

233 def bic(self, X): 

234 """ 

235 Compute the Bayesian Information Criterion. 

236 

237 :param X: Data points, shape (n_samples, n_features). 

238 :type X: numpy.ndarray 

239 :return: BIC score. 

240 :rtype: float 

241 """ 

242 X_trans = self._transform_params_forward(X) 

243 return self.gm.bic(X_trans) 

244 

245 def aic(self, X): 

246 """ 

247 Compute the Akaike Information Criterion. 

248 

249 :param X: Data points, shape (n_samples, n_features). 

250 :type X: numpy.ndarray 

251 :return: AIC score. 

252 :rtype: float 

253 """ 

254 X_trans = self._transform_params_forward(X) 

255 return self.gm.aic(X_trans) 

256 

257 def _transform_params_forward(self, X): 

258 """ 

259 Transform parameters from original to unbounded space. 

260 

261 :param X: Data in original space. 

262 :type X: numpy.ndarray 

263 :return: Data in transformed space. 

264 :rtype: numpy.ndarray 

265 """ 

266 X_trans = X.copy() 

267 for i in range(X.shape[1]): 

268 X_trans[:, i] = trans_fwd( 

269 x=np.array(X[:, i]), 

270 param_bounds=self.param_bounds[i], 

271 param_bounds_trans=self.bounds_trans, 

272 ) 

273 

274 return X_trans 

275 

276 def _transform_params_inverse(self, X_trans): 

277 """ 

278 Transform parameters from unbounded back to original space. 

279 

280 :param X_trans: Data in transformed space. 

281 :type X_trans: numpy.ndarray 

282 :return: Data in original space. 

283 :rtype: numpy.ndarray 

284 """ 

285 X = X_trans.copy() 

286 for i in range(X.shape[1]): 

287 X[:, i] = trans_inv( 

288 x=np.array(X_trans[:, i]), 

289 param_bounds=self.param_bounds[i], 

290 param_bounds_trans=self.bounds_trans, 

291 ) 

292 

293 return X