Coverage for python/lsst/cp/pipe/ptc/astierCovFitParameters.py: 22%
119 statements
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-05 18:59 -0800
« prev ^ index » next coverage.py v7.1.0, created at 2023-02-05 18:59 -0800
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/>.
22# Code developed at LPNHE ("saunerie", Paris)
24import numpy as np
26__all__ = ['FitParameters']
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).
34File: bfptc/py/fitparameters.py
35Commit hash: d46ba836fd5feb1c0065b61472c5f31b73b8480f
38Notes from original code
39-----------------------
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:
45- `StructArray`: a derivative of numpy class ndarray to manage large
46vectors organized into named subgroups.
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.
52"""
55class Structure(object):
56 """ Collection of named slices
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.
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
98 def __getitem__(self, arg):
99 if isinstance(arg, str):
100 return self.slices[arg]
101 else:
102 return arg
104 def __iter__(self):
105 return self.slices.__iter__()
107 def __len__(self):
108 return self._len
111class StructArray(np.ndarray):
112 """Decorate numpy arrays with a collection of named slices.
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.
118 Examples:
119 ---------
120 >>> v = StructArray(np.zeros(10), [('a', 3), ('b', 7)])
121 >>> print v['a']
122 [ 0. 0. 0.]
124 >>> C = StructArray(np.zeros((10,10)), [('a', 2), ('b', 8)])
125 >>> print C['a', 'a']
126 [[ 0. 0.]
127 [ 0. 0.]]
128 """
130 def __new__(cls, array, struct=[]):
131 obj = array.view(cls)
132 obj.struct = Structure(struct)
133 return obj
135 def __array_finalize__(self, obj):
136 if obj is None:
137 return
138 self.struct = getattr(obj, 'struct', [])
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]
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
152 def __getslice__(self, start, stop):
153 return self.__getitem__(slice(start, stop))
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)
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])
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.
174 The parameters can be organized in named slices (block of contiguous
175 values) accessible through indexing by their name as in `StructArray`.
177 >>> p_salt = FitParameters(['X0', 'X1', 'Color', 'Redshift'])
178 >>> p_dice = FitParameters([('alpha', 2), ('S', 10), ('dSdT', 10), 'idark']) # noqa: W505
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:
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])
197 It is also possible to mark parameters as fixed to a value.
198 >>> p_dice.fix(0, 12.)
200 Value is optional. The above is equivalent to:
201 >>> p_dice[0] = 12.
202 >>> p_dice.fix(0)
204 Again named slices simplifies the operations:
205 >>> p_dice['S'].fix([0, -1], 12.)
206 >>> p_dice['dSdT'].fix([0, -1])
208 One can fix entire slices at once:
209 >>> p_dice['idark'].fix()
210 >>> p_salt['Redshift'].fix(val=0.23432)
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
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
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.]
230 >>> p_dice[:5].free = 4.
231 >>> print p_dice[:5].free
232 [ 4. 4. 4.]
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
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 """
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
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
266 def _reindex(self):
267 self._index[self._free] = np.arange(self._free.sum())
268 self._index[~self._free] = -1
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()
276 def release(self, keys=slice(None)):
277 self._free[keys] = True
278 self._base._reindex()
280 def indexof(self, indices=slice(None)):
281 return self._index[indices]
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
294 def __setitem__(self, args, val):
295 self._pars[args] = val
297 @property
298 def free(self):
299 return self._pars[self._free]
301 @free.setter
302 def free(self, val):
303 self._pars[self._free] = val
305 @property
306 def full(self):
307 return self._pars
309 @full.setter
310 def full(self, val):
311 self._pars = val
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