Coverage for pesummary/utils/array.py: 88.6%

140 statements  

« prev     ^ index     » next       coverage.py v7.4.4, created at 2024-05-02 08:42 +0000

1# Licensed under an MIT style license -- see LICENSE.md 

2 

3import numpy as np 

4from pesummary.utils.decorators import deprecation 

5 

6__author__ = ["Charlie Hoy <charlie.hoy@ligo.org>"] 

7 

8 

9def _2DArray(input_array, likelihood=None, prior=None, weights=None): 

10 """Helper function for initialising multiple Array objects. 

11 

12 Parameters 

13 ---------- 

14 input_array: np.ndarray, list 

15 input list/array 

16 likelihood: list, optional 

17 log likelihood samples to use when calculating maximum likelihood 

18 prior: list, optional 

19 log prior samples to use when calculating maximum posterior 

20 weights: list, optional 

21 weights to use for the samples 

22 

23 Returns 

24 ------- 

25 array: list 

26 list of Array objects of size len(input_array) 

27 """ 

28 obj = np.atleast_2d(input_array) 

29 if obj.ndim > 2: 

30 raise ValueError("Input must be one- or two-dimensional") 

31 if not obj.shape[-1]: 

32 standard_deviation, minimum, maximum = [None], [None], [None] 

33 else: 

34 standard_deviation = np.std(obj, axis=1) 

35 minimum = np.min(obj, axis=1) 

36 maximum = np.max(obj, axis=1) 

37 try: 

38 maxL = Array._maxL(obj.T, likelihood=likelihood) 

39 except Exception: 

40 maxL = None 

41 try: 

42 maxP = Array._maxP(obj.T, log_likelihood=likelihood, log_prior=prior) 

43 except Exception: 

44 maxP = None 

45 return [ 

46 Array( 

47 _array, minimum=minimum[num], maximum=maximum[num], 

48 standard_deviation=standard_deviation[num], 

49 maxL=maxL[num] if maxL is not None else None, 

50 maxP=maxP[num] if maxP is not None else None, weights=weights 

51 ) for num, _array in enumerate(obj) 

52 ] 

53 

54 

55class Array(np.ndarray): 

56 """Class to add extra functions and methods to np.ndarray 

57 

58 Parameters 

59 ---------- 

60 input_aray: list/array 

61 input list/array 

62 

63 Attributes 

64 ---------- 

65 median: float 

66 median of the input array 

67 mean: float 

68 mean of the input array 

69 key_data: dict 

70 dictionary containing the key data associated with the array 

71 """ 

72 __slots__ = [ 

73 "standard_deviation", "minimum", "maximum", "maxL", "maxP", "weights" 

74 ] 

75 

76 def __new__( 

77 cls, input_array, standard_deviation=None, minimum=None, maximum=None, 

78 maxL=None, maxP=None, likelihood=None, prior=None, 

79 weights=None 

80 ): 

81 obj = np.asarray(input_array).view(cls) 

82 obj.weights = weights 

83 mapping = { 

84 "standard_deviation": [standard_deviation, np.std, {}], 

85 "minimum": [minimum, np.min, {}], 

86 "maximum": [maximum, np.max, {}], 

87 "maxL": [maxL, cls._maxL, {"likelihood": likelihood}], 

88 "maxP": [ 

89 maxP, cls._maxP, 

90 {"log_likelihood": likelihood, "log_prior": prior} 

91 ], 

92 } 

93 for attr, item in mapping.items(): 

94 if item[0] is None: 

95 try: 

96 setattr(obj, attr, item[1](obj, **item[2])) 

97 except Exception: 

98 setattr(obj, attr, None) 

99 else: 

100 setattr(obj, attr, item[0]) 

101 return obj 

102 

103 def __reduce__(self): 

104 pickled_state = super(Array, self).__reduce__() 

105 new_state = pickled_state[2] + tuple( 

106 [getattr(self, i) for i in self.__slots__] 

107 ) 

108 return (pickled_state[0], pickled_state[1], new_state) 

109 

110 def __setstate__(self, state): 

111 self.standard_deviation = state[-6] 

112 self.minimum = state[-5] 

113 self.maximum = state[-4] 

114 self.maxL = state[-3] 

115 self.maxP = state[-2] 

116 self.weights = state[-1] 

117 super(Array, self).__setstate__(state[0:-6]) 

118 

119 def average(self, type="mean"): 

120 """Return the average of the array 

121 

122 Parameters 

123 ---------- 

124 type: str 

125 the method to average the array 

126 """ 

127 if type == "mean": 

128 return self._mean(self, weights=self.weights) 

129 elif type == "median": 

130 return self._median(self, weights=self.weights) 

131 else: 

132 return None 

133 

134 @staticmethod 

135 def _mean(array, weights=None): 

136 """Compute the mean from a set of weighted samples 

137 

138 Parameters 

139 ---------- 

140 array: np.ndarray 

141 input array 

142 weights: np.ndarray, optional 

143 list of weights associated with each sample 

144 """ 

145 if weights is None: 

146 return np.mean(array) 

147 weights = np.array(weights).flatten() / float(sum(weights)) 

148 return float(np.dot(np.array(array), weights)) 

149 

150 @staticmethod 

151 def _median(array, weights=None): 

152 """Compute the median from a set of weighted samples 

153 

154 Parameters 

155 ---------- 

156 array: np.ndarray 

157 input array 

158 weights: np.ndarray, optional 

159 list of weights associated with each sample 

160 """ 

161 from pesummary.utils.credible_interval import credible_interval 

162 if weights is None: 

163 return np.median(array) 

164 return credible_interval(array, 50, weights=weights) 

165 

166 @staticmethod 

167 def _maxL(array, likelihood=None): 

168 """Return the maximum likelihood value of the array 

169 

170 Parameters 

171 ---------- 

172 array: np.ndarray 

173 input array 

174 likelihood: np.ndarray, optional 

175 likelihoods associated with each sample 

176 """ 

177 if likelihood is not None: 

178 likelihood = list(likelihood) 

179 ind = likelihood.index(np.max(likelihood)) 

180 return array[ind] 

181 return None 

182 

183 @staticmethod 

184 def _maxP(array, log_likelihood=None, log_prior=None): 

185 """Return the maximum posterior value of the array 

186 

187 Parameters 

188 ---------- 

189 array: np.ndarray 

190 input array 

191 log_likelihood: np.ndarray, optional 

192 log likelihoods associated with each sample 

193 log_prior: np.ndarray, optional 

194 log prior associated with each sample 

195 """ 

196 if any(param is None for param in [log_likelihood, log_prior]): 

197 return None 

198 likelihood = np.array(log_likelihood) 

199 prior = np.array(log_prior) 

200 posterior = likelihood + prior 

201 ind = np.argmax(posterior) 

202 return array[ind] 

203 

204 def to_dtype(self, _dtype): 

205 return _dtype(self) 

206 

207 @property 

208 def key_data(self): 

209 return self._key_data(self) 

210 

211 @staticmethod 

212 def _key_data( 

213 array, header=[ 

214 "mean", "median", "std", "maxL", "maxP", "5th percentile", 

215 "95th percentile" 

216 ] 

217 ): 

218 """Return a dictionary containing the key data associated with the 

219 array 

220 

221 Parameters 

222 ---------- 

223 array: np.ndarray 

224 input array 

225 header: list 

226 list of properties you wish to return 

227 """ 

228 def safe_dtype_change(array, _dtype): 

229 if array is not None: 

230 if isinstance(array, Array): 

231 return array.to_dtype(_dtype) 

232 else: 

233 return _dtype(array) 

234 return array 

235 

236 mydict = {} 

237 for key in header: 

238 if not hasattr(np.ndarray, key): 

239 try: 

240 _value = safe_dtype_change(getattr(array, key), float) 

241 except AttributeError: 

242 if key == "5th percentile": 

243 _value = safe_dtype_change( 

244 array.credible_interval(percentile=5), float 

245 ) 

246 elif key == "95th percentile": 

247 _value = safe_dtype_change( 

248 array.credible_interval(percentile=95), float 

249 ) 

250 else: 

251 _value = safe_dtype_change( 

252 array.average(type=key), float 

253 ) 

254 else: 

255 if key == "std": 

256 _value = safe_dtype_change(array.standard_deviation, float) 

257 else: 

258 _value = safe_dtype_change(array.average(type=key), float) 

259 mydict[key] = _value 

260 return mydict 

261 

262 @staticmethod 

263 @deprecation( 

264 "The Array.percentile method will be deprecated in future releases. Please " 

265 "use pesummary.utils.credible_interval.credible_interval instead" 

266 ) 

267 def percentile(array, weights=None, percentile=None): 

268 from pesummary.utils.credible_interval import credible_interval 

269 return credible_interval(array, percentile, weights=weights) 

270 

271 @deprecation( 

272 "The Array.confidence_interval method will be deprecated in future releases. " 

273 "Please use Array.credible_interval instead" 

274 ) 

275 def confidence_interval(self, percentile=None): 

276 return self.credible_interval(percentile=percentile) 

277 

278 def credible_interval(self, percentile=None): 

279 """Return the credible interval of the array 

280 

281 Parameters 

282 ---------- 

283 percentile: int/list, optional 

284 Percentile or sequence of percentiles to compute, which must be 

285 between 0 and 100 inclusive 

286 """ 

287 from pesummary.utils.credible_interval import credible_interval 

288 if percentile is not None: 

289 return credible_interval(self, percentile, weights=self.weights) 

290 return self.credible_interval(percentile=[5, 95]) 

291 

292 def two_sided_credible_interval(self, percentile=90): 

293 """Return the two-sided credible interval of the array 

294 

295 Parameters 

296 ---------- 

297 percentile: float, optional 

298 Percentile to compute. Must be between 0 and 100 inclusive. Default 90 

299 """ 

300 from pesummary.utils.credible_interval import two_sided_credible_interval 

301 return two_sided_credible_interval(self, percentile, weights=self.weights) 

302 

303 def __array_finalize__(self, obj): 

304 if obj is None: 

305 return 

306 self.standard_deviation = getattr(obj, 'standard_deviation', None) 

307 self.minimum = getattr(obj, 'minimum', None) 

308 self.maximum = getattr(obj, 'maximum', None) 

309 self.maxL = getattr(obj, 'maxL', None) 

310 self.maxP = getattr(obj, 'maxP', None) 

311 self.weights = getattr(obj, 'weights', None) 

312 

313 def to_numpy(self): 

314 """Convert Array object to a numpy.ndarray 

315 

316 Returns 

317 ------- 

318 data: np.ndarray/tuple 

319 return stored data as a np.ndarray. If weights are stored in the 

320 Array object, return a tuple containing the stored data as a 

321 np.ndarray and the weights as a np.ndarray 

322 """ 

323 _array = np.asarray(self) 

324 if self.weights is None: 

325 return _array 

326 return _array, np.asarray(self.weights)