1 | import sys, math, time |
---|
2 | import numpy |
---|
3 | |
---|
4 | from formatnum import format_uncertainty |
---|
5 | |
---|
6 | class FitHandler(object): |
---|
7 | """ |
---|
8 | Abstract interface for fit thread handler. |
---|
9 | |
---|
10 | The methods in this class are called by the optimizer as the fit |
---|
11 | progresses. |
---|
12 | |
---|
13 | Note that it is up to the optimizer to call the fit handler correctly, |
---|
14 | reporting all status changes and maintaining the 'done' flag. |
---|
15 | """ |
---|
16 | done = False |
---|
17 | """True when the fit job is complete""" |
---|
18 | result = None |
---|
19 | """The current best result of the fit""" |
---|
20 | |
---|
21 | def improvement(self): |
---|
22 | """ |
---|
23 | Called when a result is observed which is better than previous |
---|
24 | results from the fit. |
---|
25 | |
---|
26 | result is a FitResult object, with parameters, #calls and fitness. |
---|
27 | """ |
---|
28 | def error(self, msg): |
---|
29 | """ |
---|
30 | Model had an error; print traceback |
---|
31 | """ |
---|
32 | def progress(self, current, expected): |
---|
33 | """ |
---|
34 | Called each cycle of the fit, reporting the current and the |
---|
35 | expected amount of work. The meaning of these values is |
---|
36 | optimizer dependent, but they can be converted into a percent |
---|
37 | complete using (100*current)//expected. |
---|
38 | |
---|
39 | Progress is updated each iteration of the fit, whatever that |
---|
40 | means for the particular optimization algorithm. It is called |
---|
41 | after any calls to improvement for the iteration so that the |
---|
42 | update handler can control I/O bandwidth by suppressing |
---|
43 | intermediate improvements until the fit is complete. |
---|
44 | """ |
---|
45 | def finalize(self): |
---|
46 | """ |
---|
47 | Fit is complete; best results are reported |
---|
48 | """ |
---|
49 | def abort(self): |
---|
50 | """ |
---|
51 | Fit was aborted. |
---|
52 | """ |
---|
53 | |
---|
54 | class ConsoleUpdate(FitHandler): |
---|
55 | """ |
---|
56 | Print progress to the console. |
---|
57 | """ |
---|
58 | isbetter = False |
---|
59 | """Record whether results improved since last update""" |
---|
60 | progress_delta = 60 |
---|
61 | """Number of seconds between progress updates""" |
---|
62 | improvement_delta = 5 |
---|
63 | """Number of seconds between improvement updates""" |
---|
64 | def __init__(self,quiet=False,progress_delta=60,improvement_delta=5): |
---|
65 | """ |
---|
66 | If quiet is true, only print out final summary, not progress and |
---|
67 | improvements. |
---|
68 | """ |
---|
69 | #import traceback; traceback.print_stack() |
---|
70 | self.progress_time = time.time() |
---|
71 | self.progress_percent = 0 |
---|
72 | self.improvement_time = self.progress_time |
---|
73 | self.isbetter = False |
---|
74 | self.quiet = quiet |
---|
75 | self.progress_delta = progress_delta |
---|
76 | self.improvement_delta = improvement_delta |
---|
77 | |
---|
78 | def progress(self, k, n): |
---|
79 | """ |
---|
80 | Report on progress. |
---|
81 | """ |
---|
82 | if self.quiet: return |
---|
83 | t = time.time() |
---|
84 | p = int((100*k)//n) |
---|
85 | |
---|
86 | # Show improvements if there are any |
---|
87 | dt = t - self.improvement_time |
---|
88 | if self.isbetter and dt > self.improvement_delta: |
---|
89 | self.result.print_summary() |
---|
90 | self.isbetter = False |
---|
91 | self.improvement_time = t |
---|
92 | |
---|
93 | # Update percent complete |
---|
94 | dp = p-self.progress_percent |
---|
95 | if dp < 1: return |
---|
96 | dt = t - self.progress_time |
---|
97 | if dt > self.progress_delta: |
---|
98 | if 1 <= dp <= 2: |
---|
99 | print "%d%% complete"%p |
---|
100 | self.progress_percent = p |
---|
101 | self.progress_time = t |
---|
102 | elif 2 < dp <= 5: |
---|
103 | if p//5 != self.progress_percent//5: |
---|
104 | print "%d%% complete"%(5*(p//5)) |
---|
105 | self.progress_percent = p |
---|
106 | self.progress_time = t |
---|
107 | else: |
---|
108 | if p//10 != self.progress_percent//10: |
---|
109 | print "%d%% complete"%(10*(p//10)) |
---|
110 | self.progress_percent = p |
---|
111 | self.progress_time = t |
---|
112 | |
---|
113 | def improvement(self): |
---|
114 | """ |
---|
115 | Called when a result is observed which is better than previous |
---|
116 | results from the fit. |
---|
117 | """ |
---|
118 | self.isbetter = True |
---|
119 | |
---|
120 | def error(self, msg): |
---|
121 | """ |
---|
122 | Model had an error; print traceback |
---|
123 | """ |
---|
124 | if self.isbetter: |
---|
125 | self.result.print_summary() |
---|
126 | print msg |
---|
127 | |
---|
128 | def finalize(self): |
---|
129 | if self.isbetter: |
---|
130 | self.result.print_summary() |
---|
131 | print "Total function calls:",self.result.calls |
---|
132 | |
---|
133 | def abort(self): |
---|
134 | if self.isbetter: |
---|
135 | self.result.print_summary() |
---|
136 | |
---|
137 | |
---|
138 | class FitParameter(object): |
---|
139 | """ |
---|
140 | Fit result for an individual parameter. |
---|
141 | """ |
---|
142 | def __init__(self, name, range, value): |
---|
143 | self.name = name |
---|
144 | self.range = range |
---|
145 | self.value = value |
---|
146 | self.stderr = None |
---|
147 | def summarize(self): |
---|
148 | """ |
---|
149 | Return parameter range string. |
---|
150 | |
---|
151 | E.g., " Gold .....|.... 5.2043 in [2,7]" |
---|
152 | """ |
---|
153 | bar = ['.']*10 |
---|
154 | lo,hi = self.range |
---|
155 | if numpy.isfinite(lo)and numpy.isfinite(hi): |
---|
156 | portion = (self.value-lo)/(hi-lo) |
---|
157 | if portion < 0: portion = 0. |
---|
158 | elif portion >= 1: portion = 0.99999999 |
---|
159 | barpos = int(math.floor(portion*len(bar))) |
---|
160 | bar[barpos] = '|' |
---|
161 | bar = "".join(bar) |
---|
162 | lostr = "[%g"%lo if numpy.isfinite(lo) else "(-inf" |
---|
163 | histr = "%g]"%hi if numpy.isfinite(hi) else "inf)" |
---|
164 | valstr = format_uncertainty(self.value, self.stderr) |
---|
165 | return "%25s %s %s in %s,%s" % (self.name,bar,valstr,lostr,histr) |
---|
166 | def __repr__(self): |
---|
167 | return "FitParameter('%s')"%self.name |
---|
168 | |
---|
169 | class FitResult(object): |
---|
170 | """ |
---|
171 | Container for reporting fit results. |
---|
172 | """ |
---|
173 | def __init__(self, parameters, calls, fitness): |
---|
174 | self.parameters = parameters |
---|
175 | """Fit parameter list, each with name, range and value attributes.""" |
---|
176 | self.calls = calls |
---|
177 | """Number of function calls""" |
---|
178 | self.fitness = fitness |
---|
179 | """Value of the goodness of fit metric""" |
---|
180 | self.pvec = numpy.array([p.value for p in self.parameters]) |
---|
181 | """Parameter vector""" |
---|
182 | self.stderr = None |
---|
183 | """Parameter uncertainties""" |
---|
184 | self.cov = None |
---|
185 | """Covariance matrix""" |
---|
186 | |
---|
187 | def update(self, pvec, fitness, calls): |
---|
188 | self.calls = calls |
---|
189 | self.fitness = fitness |
---|
190 | self.pvec = pvec.copy() |
---|
191 | for i,p in enumerate(self.parameters): |
---|
192 | p.value = pvec[i] |
---|
193 | |
---|
194 | def calc_cov(self, fn): |
---|
195 | """ |
---|
196 | Return the covariance matrix inv(J'J) at point p. |
---|
197 | """ |
---|
198 | if hasattr(fn, 'jacobian'): |
---|
199 | # Find cov of f at p |
---|
200 | # cov(f,p) = inv(J'J) |
---|
201 | # Use SVD |
---|
202 | # J = U S V' |
---|
203 | # J'J = (U S V')' (U S V') |
---|
204 | # = V S' U' U S V' |
---|
205 | # = V S S V' |
---|
206 | # inv(J'J) = inv(V S S V') |
---|
207 | # = inv(V') inv(S S) inv(V) |
---|
208 | # = V inv (S S) V' |
---|
209 | J = fn.jacobian(self.pvec) |
---|
210 | u,s,vh = numpy.linalg.svd(J,0) |
---|
211 | JTJinv = numpy.dot(vh.T.conj()/s**2,vh) |
---|
212 | self.set_cov(JTJinv) |
---|
213 | |
---|
214 | def set_cov(self, cov): |
---|
215 | """ |
---|
216 | Return the covariance matrix inv(J'J) at point p. |
---|
217 | """ |
---|
218 | self.cov = cov |
---|
219 | if cov is not None: |
---|
220 | self.stderr = numpy.sqrt(numpy.diag(self.cov)) |
---|
221 | # Set the uncertainties on the individual parameters |
---|
222 | for k,p in enumerate(self.parameters): |
---|
223 | p.stderr = self.stderr[k] |
---|
224 | else: |
---|
225 | self.stderr = None |
---|
226 | # Reset the uncertainties on the individual parameters |
---|
227 | for k,p in enumerate(self.parameters): |
---|
228 | p.stderr = None |
---|
229 | |
---|
230 | def __str__(self): |
---|
231 | #import traceback; traceback.print_stack() |
---|
232 | if self.parameters == None: return "No results" |
---|
233 | L = ["P%-3d %s"%(n+1,p.summarize()) for n,p in enumerate(self.parameters)] |
---|
234 | L.append("=== goodness of fit: %g"%(self.fitness)) |
---|
235 | return "\n".join(L) |
---|
236 | |
---|
237 | def print_summary(self, fid=sys.stdout): |
---|
238 | print >>fid, self |
---|
239 | |
---|