[3570545] | 1 | # This program is public domain |
---|
| 2 | """ |
---|
| 3 | Fitting service interface. |
---|
| 4 | |
---|
| 5 | A fit consists of a set of models and a fitting engine. The models are |
---|
| 6 | collected in an assembly, which manages the parameter set and the |
---|
| 7 | constraints between them. The models themselves are tightly coupled |
---|
| 8 | to the data that they are modeling and the data is invisible to the fit. |
---|
| 9 | |
---|
| 10 | The fitting engine can use a variety of methods depending on model. |
---|
| 11 | |
---|
| 12 | |
---|
| 13 | Usage |
---|
| 14 | ===== |
---|
| 15 | |
---|
| 16 | The fitter can be run directly on the local machine:: |
---|
| 17 | |
---|
| 18 | import park |
---|
| 19 | M1 = park.models.Peaks(datafile=park.sampledata('peak.dat')) |
---|
| 20 | M1.add_peak('P1', 'gaussian', A=[4,6], mu=[0.2, 0.5], sigma=0.1) |
---|
| 21 | result = park.fit(models=[M1]) |
---|
| 22 | print result |
---|
| 23 | |
---|
| 24 | The default settings print results every time the fit improves, and |
---|
| 25 | print a global result when the fit is complete. This is a suitable |
---|
| 26 | interface for a fitting script. |
---|
| 27 | |
---|
| 28 | For larger fit jobs you will want to run the fit on a remote server. |
---|
| 29 | The model setup is identical, but the fit call is different:: |
---|
| 30 | |
---|
| 31 | service = park.FitService('server:port') |
---|
| 32 | result = park.fit(models=[M1], service=service) |
---|
| 33 | print result |
---|
| 34 | |
---|
| 35 | Again, the default settings print results every time the fit improves, |
---|
| 36 | and print a global result when the fit is complete. |
---|
| 37 | |
---|
| 38 | For long running fit jobs, you want to be able to disconnect from |
---|
| 39 | the server after submitting the job, and later reconnect to fetch |
---|
| 40 | the results. An additional email field will send notification by |
---|
| 41 | email when the fit starts and ends, and daily updates on the status |
---|
| 42 | of all fits:: |
---|
| 43 | |
---|
| 44 | service = park.FitService('server:port') |
---|
| 45 | service.notify(email='me@my.email.address',update='daily') |
---|
| 46 | fit = park.Fit(models=[M1]) |
---|
| 47 | id = service.submit_job(fit, jobname='peaks') |
---|
| 48 | print id |
---|
| 49 | |
---|
| 50 | The results can be retrieved either by id returned from the server, |
---|
| 51 | or by the given jobname:: |
---|
| 52 | |
---|
| 53 | import park |
---|
| 54 | service = park.FitService('server:port',user='userid') |
---|
| 55 | fitlist = service.retrieve('peaks') |
---|
| 56 | for fit in fitlist: |
---|
| 57 | print fit.summary() |
---|
| 58 | |
---|
| 59 | The fit itself is a complicated object, including the model, the |
---|
| 60 | optimizer, and the type of uncertainty analysis to perform. |
---|
| 61 | |
---|
| 62 | GUI Usage |
---|
| 63 | ========= |
---|
| 64 | |
---|
| 65 | When used from a graphical user interface, a different programming |
---|
| 66 | interface is needed. In this case, the user may want to watch |
---|
| 67 | the progress of the fit and perhaps stop it. Also, as fits can |
---|
| 68 | take some time to complete, the user would like to be able to |
---|
| 69 | set up additional fits and run them at the same time, switching |
---|
| 70 | between them as necessary to monitor progress. |
---|
| 71 | |
---|
| 72 | """ |
---|
| 73 | import time, thread |
---|
| 74 | |
---|
| 75 | import numpy |
---|
| 76 | |
---|
| 77 | import assembly, fitresult |
---|
| 78 | |
---|
| 79 | class Objective(object): |
---|
| 80 | """ |
---|
| 81 | Abstract interface to the fitness function for the park minimizer |
---|
| 82 | classes. |
---|
| 83 | |
---|
| 84 | Park provides a specific implementation `park.assembly.Assembly`. |
---|
| 85 | |
---|
| 86 | TODO: add a results() method to return model specific info to the |
---|
| 87 | TODO: fit handler. |
---|
| 88 | """ |
---|
| 89 | def residuals(self, p): |
---|
| 90 | """ |
---|
| 91 | Some fitters, notably Levenberg-Marquardt, operate directly on the |
---|
| 92 | residuals vector. If the individual residuals are not available, |
---|
| 93 | then LM cannot be used. |
---|
| 94 | |
---|
| 95 | This method is optional. |
---|
| 96 | """ |
---|
| 97 | raise NotImplementedError |
---|
| 98 | |
---|
| 99 | def residuals_deriv(self, p): |
---|
| 100 | """ |
---|
| 101 | Returns residuals and derivatives with respect to the given |
---|
| 102 | parameters. |
---|
| 103 | |
---|
| 104 | If these are unavailable in the model, then they can be approximated |
---|
| 105 | by numerical derivatives, though it is generally better to use a |
---|
| 106 | derivative free optimizer such as coliny or cobyla which can use the |
---|
| 107 | function evaluations more efficiently. In any case, your objective |
---|
| 108 | function is responsible for calculating these. |
---|
| 109 | |
---|
| 110 | This method is optional. |
---|
| 111 | """ |
---|
| 112 | raise NotImplementedError |
---|
| 113 | |
---|
| 114 | def fit_parameters(self): |
---|
| 115 | """ |
---|
| 116 | Returns a list of fit parameters. Each parameter has a name, |
---|
| 117 | an initial value and a range. |
---|
| 118 | |
---|
| 119 | See `park.fitresult.FitParameter` for an example. |
---|
| 120 | |
---|
| 121 | On each function evaluation a new parameter set will be passed |
---|
| 122 | to the fitter, with values in the same order as the list of |
---|
| 123 | parameters. |
---|
| 124 | """ |
---|
| 125 | raise NotImplementedError |
---|
| 126 | |
---|
| 127 | def __call__(self, p): |
---|
| 128 | """ |
---|
| 129 | Returns the objective value for parameter set p . |
---|
| 130 | """ |
---|
| 131 | raise NotImplementedError |
---|
| 132 | |
---|
| 133 | def abort(self): |
---|
| 134 | """ |
---|
| 135 | Halts the current function evaluation, and has it return inf. |
---|
| 136 | This will be called from a separate thread. If the function |
---|
| 137 | contains an expensive calculation, it should reset an abort |
---|
| 138 | flag before each evaluation and test it periodically. |
---|
| 139 | |
---|
| 140 | This method is optional. |
---|
| 141 | """ |
---|
| 142 | |
---|
| 143 | class Fitter(object): |
---|
| 144 | """Abstract interface for a fitness optimizer. |
---|
| 145 | |
---|
| 146 | A fitter has a single method, fit, which takes an objective |
---|
| 147 | function (`park.fit.Objective`) and a handler. |
---|
| 148 | |
---|
| 149 | For a concrete instance see `park.fitmc.FitMC`. |
---|
| 150 | """ |
---|
| 151 | def __init__(self, **kw): |
---|
| 152 | for k,v in kw.items(): |
---|
| 153 | if hasattr(self,k): |
---|
| 154 | setattr(self,k,v) |
---|
| 155 | else: |
---|
| 156 | raise AttributeError(k+" is not an attribute of "+self.__class__.__name__) |
---|
| 157 | |
---|
| 158 | def _threaded(self, fn, *args, **kw): |
---|
| 159 | thread.start_new_thread(fn,args,kw) |
---|
| 160 | |
---|
| 161 | |
---|
| 162 | def _fit(self, objective, x0, bounds): |
---|
| 163 | """ |
---|
| 164 | Run the actual fit in a separate thread |
---|
| 165 | |
---|
| 166 | Each cycle k of n: |
---|
| 167 | self.handler.progress(k,n) |
---|
| 168 | Each improvement: |
---|
| 169 | self.handler.result.update(x,fx,ncalls) |
---|
| 170 | self.handler.improvement() |
---|
| 171 | On completion (if not already performed): |
---|
| 172 | self.hander.result.update(x,fx,ncalls) |
---|
| 173 | self.handler.done |
---|
| 174 | self.handler.finalize() |
---|
| 175 | """ |
---|
| 176 | raise NotImplementedError |
---|
| 177 | |
---|
| 178 | def fit(self, fitness, handler): |
---|
| 179 | """ |
---|
| 180 | Global optimizer. |
---|
| 181 | |
---|
| 182 | This function should return immediately |
---|
| 183 | """ |
---|
| 184 | # Determine initial value and bounds |
---|
| 185 | pars = fitness.fit_parameters() |
---|
| 186 | bounds = numpy.array([p.range for p in pars]).T |
---|
| 187 | x0 = [p.value for p in pars] |
---|
| 188 | |
---|
| 189 | # Initialize the monitor and results. |
---|
| 190 | # Need to make our own copy of the fit results so that the |
---|
| 191 | # values don't get stomped on by the next fit iteration. |
---|
| 192 | handler.done = False |
---|
| 193 | self.handler = handler |
---|
| 194 | fitpars = [fitresult.FitParameter(pars[i].name,pars[i].range,v) |
---|
| 195 | for i,v in enumerate(x0)] |
---|
| 196 | handler.result = fitresult.FitResult(fitpars, 0, numpy.NaN) |
---|
| 197 | |
---|
| 198 | # Run the fit (fit should perform _progress and _improvement updates) |
---|
| 199 | # This function may return before the fit is complete. |
---|
| 200 | self._fit(fitness, x0, bounds) |
---|
| 201 | |
---|
| 202 | class FitJob(object): |
---|
| 203 | """ |
---|
| 204 | Fit job. |
---|
| 205 | |
---|
| 206 | This implements `park.job.Job`. |
---|
| 207 | """ |
---|
| 208 | def __init__(self, objective=None, fitter=None, handler=None): |
---|
| 209 | self.fitter = fitter |
---|
| 210 | self.objective = objective |
---|
| 211 | self.handler = handler |
---|
| 212 | def run(self): |
---|
| 213 | self.fitter.fit(self.objective, self.handler) |
---|
| 214 | |
---|
| 215 | class LocalQueue(object): |
---|
| 216 | """ |
---|
| 217 | Simple interface to the local job queue. Currently supports start and |
---|
| 218 | wait. Needs to support stop and status. Also, needs to be a proper queue, |
---|
| 219 | and needs to allow jobs to run in separate processes according to priority, |
---|
| 220 | etc. All the essentials of the remote queuing system without the remote |
---|
| 221 | calls. |
---|
| 222 | |
---|
| 223 | Unlike the remote queue, the local queue need not maintain persistence. |
---|
| 224 | """ |
---|
| 225 | running = False |
---|
| 226 | def start(self, job): |
---|
| 227 | self.job = job |
---|
| 228 | job.run() |
---|
| 229 | return id(job) |
---|
| 230 | |
---|
| 231 | def wait(self, interval=.1): |
---|
| 232 | """ |
---|
| 233 | Wait for the job to complete. This is used in scripts to impose |
---|
| 234 | a synchronous interface to the fitting service. |
---|
| 235 | """ |
---|
| 236 | while not self.job.handler.done: |
---|
| 237 | time.sleep(interval) |
---|
| 238 | return self.job.handler.result |
---|
| 239 | |
---|
| 240 | def fit(models=None, fitter=None, service=None, handler=None): |
---|
| 241 | """ |
---|
| 242 | Start a fit with a set of models. The model set must be |
---|
| 243 | in a form accepted by `park.assembly.Assembly`. |
---|
| 244 | |
---|
| 245 | This is a convenience function which sets up the default |
---|
| 246 | optimizer and uses the local fitting engine to do the work. |
---|
| 247 | Progress reports are printed as they are received. |
---|
| 248 | |
---|
| 249 | The choice of fitter, service and handler can be specified |
---|
| 250 | by the caller. |
---|
| 251 | |
---|
| 252 | The default fitter is FitMC, which is a monte carlo Nelder-Mead |
---|
| 253 | simplex local optimizer with 100 random start points. |
---|
| 254 | |
---|
| 255 | The default handler does nothing. Instead, ConsoleUpdate could |
---|
| 256 | be used to report progress during the fit. |
---|
| 257 | |
---|
| 258 | The default service is to run in a separate thread with FitThread. |
---|
| 259 | Note that this will change soon to run in a separate process on |
---|
| 260 | the local machine so that python's global interpreter lock does |
---|
| 261 | not interfere with parallelism. |
---|
| 262 | """ |
---|
| 263 | if models is None: raise RuntimeError('fit expected a list of models') |
---|
| 264 | if service is None: service = LocalQueue() |
---|
| 265 | if fitter is None: |
---|
| 266 | import fitmc |
---|
| 267 | fitter = fitmc.FitMC() |
---|
| 268 | if handler is None: handler = fitresult.FitHandler() |
---|
| 269 | |
---|
| 270 | objective = assembly.Assembly(models) if isinstance(models,list) else models |
---|
| 271 | job = FitJob(objective,fitter,handler) |
---|
| 272 | service.start(job) |
---|
| 273 | return service.wait() |
---|
| 274 | |
---|
| 275 | |
---|
| 276 | def assembly_example(): |
---|
| 277 | import park, time |
---|
| 278 | problem = park.assembly.example() |
---|
| 279 | #result = fit(problem) |
---|
| 280 | #result.print_summary() |
---|
| 281 | handler=fitresult.ConsoleUpdate(improvement_delta=0.1,progress_delta=1) |
---|
| 282 | #result = fit(problem, handler=handler) |
---|
| 283 | result = fit(problem) |
---|
| 284 | print "=== Fit complete ===" |
---|
| 285 | result.print_summary() |
---|
| 286 | print "=== Target values ===" |
---|
| 287 | print "M1: a=1, c=1.5" |
---|
| 288 | print "M2: a=2.5, c=3" |
---|
| 289 | |
---|
| 290 | if False: # Detailed results |
---|
| 291 | print "parameter vector",result.pvec |
---|
| 292 | problem(result.pvec) |
---|
| 293 | print "residuals",problem.residuals |
---|
| 294 | for k,m in enumerate(problem.parts): |
---|
| 295 | print "Model",k,"chisq",m.chisq,"weight",m.weight |
---|
| 296 | print "pars",m.fitness.model.a,m.fitness.model.c |
---|
| 297 | print "x",m.fitness.data.fit_x |
---|
| 298 | print "y",m.fitness.data.fit_y |
---|
| 299 | print "f(x)",m.fitness.data.fx |
---|
| 300 | print "(y-f(x))/dy",m.residuals |
---|
| 301 | |
---|
| 302 | |
---|
| 303 | def demo(fitter=None): |
---|
| 304 | """Multiple minima example""" |
---|
| 305 | import time, math |
---|
| 306 | class MultiMin(object): |
---|
| 307 | def fit_parameters(self): |
---|
| 308 | return [fitresult.FitParameter('x1',[-5,5],1)] |
---|
| 309 | def __call__(self, p): |
---|
| 310 | x=p[0] |
---|
| 311 | fx = x**2 + math.sin(2*math.pi*x+3*math.pi/2) |
---|
| 312 | return fx |
---|
| 313 | handler = fitresult.ConsoleUpdate() # Show updates on the console |
---|
| 314 | handler.progress_delta = 1 # Update user every second |
---|
| 315 | handler.improvement_delta = 0.1 # Show improvements almost immediately |
---|
| 316 | fitter.fit(MultiMin(), handler) |
---|
| 317 | while not handler.done: time.sleep(1) |
---|
| 318 | |
---|
| 319 | def demo2(fitter=None): |
---|
| 320 | import park, time |
---|
| 321 | problem = park.assembly.example() |
---|
| 322 | handler = fitresult.ConsoleUpdate() # Show updates on the console |
---|
| 323 | handler.progress_delta = 1 # Update user every second |
---|
| 324 | handler.improvement_delta = 1 # Show improvements at the same rate |
---|
| 325 | fitter.fit(problem, handler) |
---|
| 326 | while not handler.done: time.sleep(1) |
---|
| 327 | |
---|
| 328 | |
---|
| 329 | |
---|
| 330 | if __name__ == "__main__": |
---|
| 331 | #main() |
---|
| 332 | assembly_example() |
---|