Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# This file is part of cp_pipe. 

2# 

3# Developed for the LSST Data Management System. 

4# This product includes software developed by the LSST Project 

5# (https://www.lsst.org). 

6# See the COPYRIGHT file at the top-level directory of this distribution 

7# for details of code ownership. 

8# 

9# This program is free software: you can redistribute it and/or modify 

10# it under the terms of the GNU General Public License as published by 

11# the Free Software Foundation, either version 3 of the License, or 

12# (at your option) any later version. 

13# 

14# This program is distributed in the hope that it will be useful, 

15# but WITHOUT ANY WARRANTY; without even the implied warranty of 

16# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 

17# GNU General Public License for more details. 

18# 

19# You should have received a copy of the GNU General Public License 

20# along with this program. If not, see <https://www.gnu.org/licenses/>. 

21 

22# Code developed at LPNHE ("saunerie", Paris) 

23 

24import numpy as np 

25 

26""" 

27The classes in this file were taken from https://github.com/PierreAstier/bfptc 

28by Pierre Astier (Laboratoire de Physique Nucléaire et de Hautes Energies (LPNHE), 

29Sorbonne Université, Paris, France). 

30 

31File: bfptc/py/fitparameters.py 

32Commit hash: d46ba836fd5feb1c0065b61472c5f31b73b8480f 

33 

34 

35Notes from original code 

36----------------------- 

37 

38This module contains utility classes to handle parameters in linear and 

39non-linear least square fits implemented in linearmodels and 

40nonlinearmodels. It provide 2 main features: 

41 

42- `StructArray`: a derivative of numpy class ndarray to manage large 

43vectors organized into named subgroups. 

44 

45- `FitParameters` : a class to manage large parameter vectors. It 

46allows to easily fix/release specific parameters or entire subgroups, 

47and remap the remaining free parameters into a contiguous vector. 

48""" 

49 

50 

51class Structure(object): 

52 """ Collection of named slices 

53 

54 Slices are specified by a name and a length. If omitted, the length 

55 default to one and the name to __0__ for the first unamed slice, __1__ 

56 for the second and so on. 

57 

58 Examples: 

59 --------- 

60 >>> s = Structure([('a', 7), 3]) 

61 >>> print len(s) 

62 10 

63 >>> for name in s: print name 

64 a 

65 __0__ 

66 >>> print len(Structure([('a', 3), 'b'])) 

67 4 

68 """ 

69 def __init__(self, groups): 

70 if isinstance(groups, Structure): 

71 self.slices = groups.slices.copy() 

72 self._len = groups._len 

73 else: 

74 self.slices = {} 

75 i = 0 

76 _n_unnamed = 0 

77 for group in groups: 

78 if isinstance(group, int): 

79 name = "__%d__" % _n_unnamed 

80 _len = group 

81 _n_unnamed += 1 

82 elif isinstance(group, str): 

83 name = group 

84 _len = 1 

85 else: 

86 try: 

87 name, _len = group 

88 except TypeError: 

89 raise TypeError('Structure specification not understood: %s' % repr(group)) 

90 self.slices[name] = slice(i, i + _len) 

91 i += _len 

92 self._len = i 

93 

94 def __getitem__(self, arg): 

95 if isinstance(arg, str): 

96 return self.slices[arg] 

97 else: 

98 return arg 

99 

100 def __iter__(self): 

101 return self.slices.__iter__() 

102 

103 def __len__(self): 

104 return self._len 

105 

106 

107class StructArray(np.ndarray): 

108 """Decorate numpy arrays with a collection of named slices. 

109 

110 Array slices becomes accessible by their name. This is applicable to 

111 nd array, although the same `Structure` is shared between all 

112 dimensions. 

113 

114 Examples: 

115 --------- 

116 >>> v = StructArray(np.zeros(10), [('a', 3), ('b', 7)]) 

117 >>> print v['a'] 

118 [ 0. 0. 0.] 

119 

120 >>> C = StructArray(np.zeros((10,10)), [('a', 2), ('b', 8)]) 

121 >>> print C['a', 'a'] 

122 [[ 0. 0.] 

123 [ 0. 0.]] 

124 """ 

125 def __new__(cls, array, struct=[]): 

126 obj = array.view(cls) 

127 obj.struct = Structure(struct) 

128 return obj 

129 

130 def __array_finalize__(self, obj): 

131 if obj is None: 

132 return 

133 self.struct = getattr(obj, 'struct', []) 

134 

135 def __getitem__(self, args): 

136 if not isinstance(args, tuple): 

137 args = args, 

138 newargs = tuple([self.struct[arg] for arg in args]) 

139 return np.asarray(self)[newargs] 

140 

141 def __setitem__(self, args, val): 

142 if not isinstance(args, tuple): 

143 args = args, 

144 newargs = tuple([self.struct[arg] for arg in args]) 

145 np.asarray(self)[newargs] = val 

146 

147 def __getslice__(self, start, stop): 

148 return self.__getitem__(slice(start, stop)) 

149 

150 def __reduce__(self): 

151 # Get the parent's __reduce__ tuple 

152 pickled_state = super(StructArray, self).__reduce__() 

153 # Create our own tuple to pass to __setstate__ 

154 new_state = pickled_state[2] + (self.struct,) 

155 # Return a tuple that replaces the parent's __setstate__ tuple with our own 

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

157 

158 def __setstate__(self, state): 

159 self.struct = state[-1] # Set the info attribute 

160 # Call the parent's __setstate__ with the other tuple elements. 

161 super(StructArray, self).__setstate__(state[0:-1]) 

162 

163 

164class FitParameters(object): 

165 """ Manages a vector of fit parameters with the possibility to mark a subset of 

166 them as fixed to a given value. 

167 

168 The parameters can be organized in named slices (block of contiguous 

169 values) accessible through indexing by their name as in `StructArray`. 

170 

171 >>> p_salt = FitParameters(['X0', 'X1', 'Color', 'Redshift']) 

172 >>> p_dice = FitParameters([('alpha', 2), ('S', 10), ('dSdT', 10), 'idark']) 

173 

174 It is possible to modify the parameters in place. Using the indexing 

175 of slices by name simplifies somewhat the operations, as one does 

176 not need to care about the position of a slice within the entire 

177 parameter vector: 

178 

179 >>> p_dice['idark'][0] = -1.0232E-12 

180 >>> p_dice['S'][4] = 25.242343E-9 

181 >>> p_dice['dSdT'][:] = 42. 

182 >>> print p_dice 

183 alpha: array([ 0., 0.]) 

184 S: array([ 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 

185 0.00000000e+00, 2.52423430e-08, 0.00000000e+00, 

186 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 

187 0.00000000e+00]) 

188 dSdT: array([ 42., 42., 42., 42., 42., 42., 42., 42., 42., 42.]) 

189 idark: array([ -1.02320000e-12]) 

190 

191 It is also possible to mark parameters as fixed to a value. 

192 >>> p_dice.fix(0, 12.) 

193 

194 Value is optional. The above is equivalent to: 

195 >>> p_dice[0] = 12. 

196 >>> p_dice.fix(0) 

197 

198 Again named slices simplifies the operations: 

199 >>> p_dice['S'].fix([0, -1], 12.) 

200 >>> p_dice['dSdT'].fix([0, -1]) 

201 

202 One can fix entire slices at once: 

203 >>> p_dice['idark'].fix() 

204 >>> p_salt['Redshift'].fix(val=0.23432) 

205 

206 The property ``full'' give access to the vector of parameters. The 

207 property "free" gives access to the free parameters: 

208 >>> print len(p_dice.free), len(p_dice.full) 

209 17 23 

210 

211 Note that free relies on fancy indexing. Access thus trigger a 

212 copy. As a consequence, the following will not actually alter the 

213 data: 

214 >>> p_dice.free[0] = 12. 

215 >>> print p_dice.free[0] 

216 0.0 

217 

218 It is still possible to set slices of free parameters as a 

219 contiguous vector. For example: 

220 >>> p_dice['S'].free = 12. 

221 >>> print p_dice['S'].free 

222 [ 12. 12. 12. 12. 12. 12. 12. 12.] 

223 

224 >>> p_dice[:5].free = 4. 

225 >>> print p_dice[:5].free 

226 [ 4. 4. 4.] 

227 

228 In particular, the typical use case which consists in updating the 

229 free parameters with the results of a fit works as expected: 

230 >>> p = np.arange(len(p_dice.free)) 

231 >>> p_dice.free = p 

232 

233 Last the class provide a convenience function that return the index 

234 of a subset of parameters in the global free parameters vector, and 

235 -1 for fixed parameters: 

236 >>> print p_dice['dSdT'].indexof() 

237 [-1 9 10 11 12 13 14 15 16 -1] 

238 >>> print p_dice['dSdT'].indexof([1,2]) 

239 [ 9 10] 

240 """ 

241 def __init__(self, groups): 

242 struct = Structure(groups) 

243 self._free = StructArray(np.ones(len(struct), dtype='bool'), struct) 

244 self._pars = StructArray(np.zeros(len(struct), dtype='float'), struct) 

245 self._index = StructArray(np.zeros(len(struct), dtype='int'), struct) 

246 self._struct = struct 

247 self._reindex() 

248 self._base = self 

249 

250 def copy(self): 

251 cop = FitParameters(self._struct) 

252 cop._free = StructArray(np.copy(self._free), cop._struct) 

253 cop._pars = StructArray(np.copy(self._pars), cop._struct) 

254 cop._index = StructArray(np.copy(self._index), cop._struct) 

255 cop._reindex() 

256 cop._base = cop 

257 return cop 

258 

259 def _reindex(self): 

260 self._index[self._free] = np.arange(self._free.sum()) 

261 self._index[~self._free] = -1 

262 

263 def fix(self, keys=slice(None), val=None): 

264 self._free[keys] = False 

265 if val is not None: 

266 self._pars[keys] = val 

267 self._base._reindex() 

268 

269 def release(self, keys=slice(None)): 

270 self._free[keys] = True 

271 self._base._reindex() 

272 

273 def indexof(self, indices=slice(None)): 

274 return self._index[indices] 

275 

276 def __getitem__(self, args): 

277 # Prevent conversion to scalars 

278 if isinstance(args, int): 

279 args = slice(args, args + 1) 

280 new = FitParameters.__new__(FitParameters) 

281 new._free = self._free[args] 

282 new._pars = self._pars[args] 

283 new._index = self._index[args] 

284 new._base = self._base 

285 return new 

286 

287 def __setitem__(self, args, val): 

288 self._pars[args] = val 

289 

290 @property 

291 def free(self): 

292 return self._pars[self._free] 

293 

294 @free.setter 

295 def free(self, val): 

296 self._pars[self._free] = val 

297 

298 @property 

299 def full(self): 

300 return self._pars 

301 

302 @full.setter 

303 def full(self, val): 

304 self._pars = val 

305 

306 def __repr__(self): 

307 if hasattr(self, '_struct'): 

308 s = "\n".join(['%s: %s' % (key, repr(self._pars[key])) for key in self._struct]) 

309 else: 

310 s = repr(self._pars) 

311 return s