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__all__ = ['FitParameters'] 

27 

28""" 

29The classes in this file were taken from 

30https://github.com/PierreAstier/bfptc by Pierre Astier (Laboratoire de 

31Physique Nucléaire et de Hautes Energies (LPNHE), Sorbonne Université, 

32Paris, France). 

33 

34File: bfptc/py/fitparameters.py 

35Commit hash: d46ba836fd5feb1c0065b61472c5f31b73b8480f 

36 

37 

38Notes from original code 

39----------------------- 

40 

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

42non-linear least square fits implemented in linearmodels and 

43nonlinearmodels. It provide 2 main features: 

44 

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

46vectors organized into named subgroups. 

47 

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

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

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

51 

52""" 

53 

54 

55class Structure(object): 

56 """ Collection of named slices 

57 

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

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

60 for the second and so on. 

61 

62 Examples: 

63 --------- 

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

65 >>> print len(s) 

66 10 

67 >>> for name in s: print name 

68 a 

69 __0__ 

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

71 4 

72 """ 

73 def __init__(self, groups): 

74 if isinstance(groups, Structure): 

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

76 self._len = groups._len 

77 else: 

78 self.slices = {} 

79 i = 0 

80 _n_unnamed = 0 

81 for group in groups: 

82 if isinstance(group, int): 

83 name = "__%d__" % _n_unnamed 

84 _len = group 

85 _n_unnamed += 1 

86 elif isinstance(group, str): 

87 name = group 

88 _len = 1 

89 else: 

90 try: 

91 name, _len = group 

92 except TypeError: 

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

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

95 i += _len 

96 self._len = i 

97 

98 def __getitem__(self, arg): 

99 if isinstance(arg, str): 

100 return self.slices[arg] 

101 else: 

102 return arg 

103 

104 def __iter__(self): 

105 return self.slices.__iter__() 

106 

107 def __len__(self): 

108 return self._len 

109 

110 

111class StructArray(np.ndarray): 

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

113 

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

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

116 dimensions. 

117 

118 Examples: 

119 --------- 

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

121 >>> print v['a'] 

122 [ 0. 0. 0.] 

123 

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

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

126 [[ 0. 0.] 

127 [ 0. 0.]] 

128 """ 

129 

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

131 obj = array.view(cls) 

132 obj.struct = Structure(struct) 

133 return obj 

134 

135 def __array_finalize__(self, obj): 

136 if obj is None: 

137 return 

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

139 

140 def __getitem__(self, args): 

141 if not isinstance(args, tuple): 

142 args = args, 

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

144 return np.asarray(self)[newargs] 

145 

146 def __setitem__(self, args, val): 

147 if not isinstance(args, tuple): 

148 args = args, 

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

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

151 

152 def __getslice__(self, start, stop): 

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

154 

155 def __reduce__(self): 

156 # Get the parent's __reduce__ tuple 

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

158 # Create our own tuple to pass to __setstate__ 

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

160 # Return a tuple that replaces the parent's __setstate__ tuple 

161 # with our own 

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

163 

164 def __setstate__(self, state): 

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

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

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

168 

169 

170class FitParameters(object): 

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

172 subset of them as fixed to a given value. 

173 

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

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

176 

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

178 >>> p_dice = FitParameters([('alpha', 2), ('S', 10), ('dSdT', 10), 'idark']) # noqa: W505 

179 

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

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

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

183 parameter vector: 

184 

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

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

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

188 >>> print p_dice 

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

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

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

192 0.00000000e+00, 0.00000000e+00, 0.00000000e+00, 

193 0.00000000e+00]) 

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

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

196 

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

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

199 

200 Value is optional. The above is equivalent to: 

201 >>> p_dice[0] = 12. 

202 >>> p_dice.fix(0) 

203 

204 Again named slices simplifies the operations: 

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

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

207 

208 One can fix entire slices at once: 

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

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

211 

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

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

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

215 17 23 

216 

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

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

219 data: 

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

221 >>> print p_dice.free[0] 

222 0.0 

223 

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

225 contiguous vector. For example: 

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

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

228 [ 12. 12. 12. 12. 12. 12. 12. 12.] 

229 

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

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

232 [ 4. 4. 4.] 

233 

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

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

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

237 >>> p_dice.free = p 

238 

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

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

241 -1 for fixed parameters: 

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

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

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

245 [ 9 10] 

246 """ 

247 

248 def __init__(self, groups): 

249 struct = Structure(groups) 

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

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

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

253 self._struct = struct 

254 self._reindex() 

255 self._base = self 

256 

257 def copy(self): 

258 cop = FitParameters(self._struct) 

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

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

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

262 cop._reindex() 

263 cop._base = cop 

264 return cop 

265 

266 def _reindex(self): 

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

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

269 

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

271 self._free[keys] = False 

272 if val is not None: 

273 self._pars[keys] = val 

274 self._base._reindex() 

275 

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

277 self._free[keys] = True 

278 self._base._reindex() 

279 

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

281 return self._index[indices] 

282 

283 def __getitem__(self, args): 

284 # Prevent conversion to scalars 

285 if isinstance(args, int): 

286 args = slice(args, args + 1) 

287 new = FitParameters.__new__(FitParameters) 

288 new._free = self._free[args] 

289 new._pars = self._pars[args] 

290 new._index = self._index[args] 

291 new._base = self._base 

292 return new 

293 

294 def __setitem__(self, args, val): 

295 self._pars[args] = val 

296 

297 @property 

298 def free(self): 

299 return self._pars[self._free] 

300 

301 @free.setter 

302 def free(self, val): 

303 self._pars[self._free] = val 

304 

305 @property 

306 def full(self): 

307 return self._pars 

308 

309 @full.setter 

310 def full(self, val): 

311 self._pars = val 

312 

313 def __repr__(self): 

314 if hasattr(self, '_struct'): 

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

316 else: 

317 s = repr(self._pars) 

318 return s