[3570545] | 1 | # This program is public domain |
---|
| 2 | """ |
---|
| 3 | An assembly is a collection of fitting functions. This provides |
---|
| 4 | the model representation that is the basis of the park fitting engine. |
---|
| 5 | |
---|
| 6 | Models can range from very simple one dimensional theory functions |
---|
| 7 | to complex assemblies of multidimensional datasets from different |
---|
| 8 | experimental techniques, each with their own theory function and |
---|
| 9 | a common underlying physical model. |
---|
| 10 | |
---|
| 11 | Usage |
---|
| 12 | ===== |
---|
| 13 | |
---|
| 14 | First define the models you want to work with. In the example |
---|
| 15 | below we will use an example of a simple multilayer system measured by |
---|
| 16 | specular reflection of xrays and neutrons. The gold depth is the only |
---|
| 17 | fitting parameter, ranging from 10-30 A. The interface depths are |
---|
| 18 | tied together using expressions. In this case the expression is |
---|
| 19 | a simple copy, but any standard math functions can be used. Some |
---|
| 20 | model developers may provide additional functions for use with the |
---|
| 21 | expression. |
---|
| 22 | |
---|
| 23 | Example models:: |
---|
| 24 | |
---|
| 25 | import reflectometry.model1d as refl |
---|
| 26 | xray = refl.model('xray') |
---|
| 27 | xray.incident('Air',rho=0) |
---|
| 28 | xray.interface('iAu',sigma=5) |
---|
| 29 | xray.layer('Au',rho=124.68,depth=[10,30]) |
---|
| 30 | xray.interface('iSi',sigma=5) |
---|
| 31 | xray.substrate('Si',rho=20.07) |
---|
| 32 | datax = refl.data('xray.dat') |
---|
| 33 | |
---|
| 34 | neutron = refl.model('neutron') |
---|
| 35 | neutron.incident('Air',rho=0) |
---|
| 36 | neutron.interface('iAu',sigma='xray.iAu') |
---|
| 37 | neutron.layer('Au',rho=4.66,depth='xray.Au.depth') |
---|
| 38 | neutron.interface('iSi',sigma='xray.iSi') |
---|
| 39 | neutron.substrate('Si',rho=2.07) |
---|
| 40 | datan = refl.data('neutron.dat') |
---|
| 41 | |
---|
| 42 | As you can see from the above, parameters can be set to a value if |
---|
| 43 | the parameter is fixed, to a range if the parametemr is fitted, or |
---|
| 44 | to a string expression if the parameter is calculated from other |
---|
| 45 | parameters. See park.Parameter.set for further details. |
---|
| 46 | |
---|
| 47 | Having constructed the models, we can now create an assembly:: |
---|
| 48 | |
---|
| 49 | import park |
---|
| 50 | assembly = park.Assembly([(xray,datax), (neutron,datan)]) |
---|
| 51 | |
---|
| 52 | Note: this would normally be done in the context of a fit |
---|
| 53 | using fit = park.Fit([(xray,datax), (neutron,datan)]), and later referenced |
---|
| 54 | using fit.assembly. |
---|
| 55 | |
---|
| 56 | Individual parts of the assembly are accessable using the |
---|
| 57 | model number 0, 1, 2... or by the model name. In the above, |
---|
| 58 | assembly[0] and assembly['xray'] refer to the same model. |
---|
| 59 | Assemblies have insert and append functions for adding new |
---|
| 60 | models, and "del model[idx]" for removing them. |
---|
| 61 | |
---|
| 62 | Once the assembly is created computing the values for the system |
---|
| 63 | is a matter of calling:: |
---|
| 64 | |
---|
| 65 | assembly.eval() |
---|
| 66 | print "Chi**2",assembly.chisq |
---|
| 67 | print "Reduced chi**2",assembly.chisq/assembly.degrees_of_freedom |
---|
| 68 | plot(arange(len(assembly.residuals)), assembly.residuals) |
---|
| 69 | |
---|
| 70 | This defines the attributes residuals, degrees_of_freedom and chisq, |
---|
| 71 | which is what the optimizer uses as the cost function to minimize. |
---|
| 72 | |
---|
| 73 | assembly.eval uses the current values for the parameters in the |
---|
| 74 | individual models. These parameters can be changed directly |
---|
| 75 | in the model. In the reflectometry example above, you could |
---|
| 76 | set the gold thickness using xray.layer.Au.depth=156, or |
---|
| 77 | something similar (the details are model specific). Parameters |
---|
| 78 | can also be changed through the assembly parameter set. In the same |
---|
| 79 | example, this would be assembly.parameterset['xray']['Au']['depth']. |
---|
| 80 | See parameter set for details. |
---|
| 81 | |
---|
| 82 | In the process of modeling data, particularly with multiple |
---|
| 83 | datasets, you will sometimes want to temporarily ignore |
---|
| 84 | how well one of the datasets matches so that you |
---|
| 85 | can more quickly refine the model for the other datasets, |
---|
| 86 | or see how particular models are influencing the fit. To |
---|
| 87 | temporarily ignore the xray data in the example above use:: |
---|
| 88 | |
---|
| 89 | assembly.parts[0].isfitted = False |
---|
| 90 | |
---|
| 91 | The model itself isn't ignored since its parameters may be |
---|
| 92 | needed to compute the parameters for other models. To |
---|
| 93 | reenable checking against the xray data, you would assign |
---|
| 94 | a True value instead. More subtle weighting of the models |
---|
| 95 | can be controlled using assembly.parts[idx].weight, but |
---|
| 96 | see below for a note on model weighting. |
---|
| 97 | |
---|
| 98 | A note on model weighting |
---|
| 99 | ------------------------- |
---|
| 100 | |
---|
| 101 | Changing the weight is equivalent to scaling the error bars |
---|
| 102 | on the given model by a factor of weight/n where n is the |
---|
| 103 | number of data points. It is better to set the correct error |
---|
| 104 | bars on your data in the first place than to adjust the weights. |
---|
| 105 | If you have the correct error bars, then you should expect |
---|
| 106 | roughly 2/3 of the data points to lie within one error bar of |
---|
| 107 | the theory curve. If consecutive data points have largely |
---|
| 108 | overlapping errorbars, then your uncertainty is overestimated. |
---|
| 109 | |
---|
| 110 | Another case where weights are adjusted (abused?) is to |
---|
| 111 | compensate for systematic errors in the data by forcing the |
---|
| 112 | errorbars to be large enough to cover the systematic bias. |
---|
| 113 | This is a poor approach to the problem. A better strategy |
---|
| 114 | is to capture the systematic effects in the model, and treat |
---|
| 115 | the measurement of the independent variable as an additional |
---|
| 116 | data point in the fit. This is still not statistically sound |
---|
| 117 | as there is likely to be a large correlation between the |
---|
| 118 | uncertainty of the measurement and the values of all the |
---|
| 119 | other variables. |
---|
| 120 | |
---|
| 121 | That said, adjusting the weight on a dataset is a quick way |
---|
| 122 | of reducing its influence on the entire fit. Please use it |
---|
| 123 | with care. |
---|
| 124 | """ |
---|
| 125 | |
---|
| 126 | __all__ = ['Assembly', 'Fitness'] |
---|
| 127 | import numpy |
---|
| 128 | |
---|
| 129 | import park |
---|
| 130 | from park.parameter import Parameter,ParameterSet |
---|
| 131 | from park.fitresult import FitParameter |
---|
| 132 | import park.expression |
---|
| 133 | |
---|
| 134 | |
---|
| 135 | |
---|
| 136 | class Fitness(object): |
---|
| 137 | """ |
---|
| 138 | Container for theory and data. |
---|
| 139 | |
---|
| 140 | The fit object compares theory with data. |
---|
| 141 | |
---|
| 142 | TODO: what to do with fittable metadata (e.g., footprint correction)? |
---|
| 143 | """ |
---|
| 144 | data = None |
---|
| 145 | model = None |
---|
| 146 | def __init__(self, model=None,data=None): |
---|
| 147 | self.data,self.model = data,model |
---|
| 148 | def _parameterset(self): |
---|
| 149 | return self.model.parameterset |
---|
| 150 | parameterset = property(_parameterset) |
---|
| 151 | def residuals(self): |
---|
| 152 | return self.data.residuals(self.model.eval) |
---|
| 153 | def residuals_deriv(self, pars=[]): |
---|
| 154 | return self.data.residuals_deriv(self.model.eval_derivs,pars=pars) |
---|
| 155 | def set(self, **kw): |
---|
| 156 | """ |
---|
| 157 | Set parameters in the model. |
---|
| 158 | |
---|
| 159 | User convenience function. This allows a user with an assembly |
---|
| 160 | of models in a script to for example set the fit range for |
---|
| 161 | parameter 'a' of the model:: |
---|
[5ab5cae] | 162 | |
---|
[3570545] | 163 | assembly[0].set(a=[5,6]) |
---|
| 164 | |
---|
| 165 | Raises KeyError if the parameter is not in parameterset. |
---|
| 166 | """ |
---|
| 167 | self.model.set(**kw) |
---|
| 168 | def abort(self): |
---|
| 169 | if hasattr(self.model,'abort'): self.model.abort() |
---|
| 170 | |
---|
| 171 | class Part(object): |
---|
| 172 | """ |
---|
| 173 | Part of a fitting assembly. Part holds the model itself and |
---|
| 174 | associated data. The part can be initialized with a fitness |
---|
| 175 | object or with a pair (model,data) for the default fitness function. |
---|
| 176 | |
---|
| 177 | fitness (Fitness) |
---|
| 178 | object implementing the `park.assembly.Fitness` interface. In |
---|
| 179 | particular, fitness should provide a parameterset attribute |
---|
| 180 | containing a ParameterSet and a residuals method returning a vector |
---|
| 181 | of residuals. |
---|
| 182 | weight (dimensionless) |
---|
| 183 | weight for the model. See comments in assembly.py for details. |
---|
| 184 | isfitted (boolean) |
---|
| 185 | True if the model residuals should be included in the fit. |
---|
| 186 | The model parameters may still be used in parameter |
---|
| 187 | expressions, but there will be no comparison to the data. |
---|
| 188 | residuals (vector) |
---|
| 189 | Residuals for the model if they have been calculated, or None |
---|
| 190 | degrees_of_freedom |
---|
| 191 | Number of residuals minus number of fitted parameters. |
---|
| 192 | Degrees of freedom for individual models does not make |
---|
| 193 | sense in the presence of expressions combining models, |
---|
| 194 | particularly in the case where a model has many parameters |
---|
| 195 | but no data or many computed parameters. The degrees of |
---|
| 196 | freedom for the model is set to be at least one. |
---|
| 197 | chisq |
---|
| 198 | sum(residuals**2); use chisq/degrees_of_freedom to |
---|
| 199 | get the reduced chisq value. |
---|
| 200 | |
---|
| 201 | Get/set the weight on the given model. |
---|
| 202 | |
---|
| 203 | assembly.weight(3) returns the weight on model 3 (0-origin) |
---|
| 204 | assembly.weight(3,0.5) sets the weight on model 3 (0-origin) |
---|
| 205 | """ |
---|
| 206 | |
---|
| 207 | def __init__(self, fitness, weight=1., isfitted=True): |
---|
| 208 | if isinstance(fitness, tuple): |
---|
| 209 | fitness = park.Fitness(*fitness) |
---|
| 210 | self.fitness = fitness |
---|
| 211 | self.weight = weight |
---|
| 212 | self.isfitted = isfitted |
---|
| 213 | self.residuals = None |
---|
| 214 | self.chisq = numpy.Inf |
---|
| 215 | self.degrees_of_freedom = 1 |
---|
| 216 | |
---|
| 217 | |
---|
| 218 | class Assembly(object): |
---|
| 219 | """ |
---|
| 220 | Collection of fit models. |
---|
| 221 | |
---|
| 222 | Assembly implements the `park.fit.Objective` interface. |
---|
| 223 | |
---|
| 224 | See `park.assembly` for usage. |
---|
| 225 | |
---|
| 226 | Instance variables: |
---|
| 227 | |
---|
| 228 | residuals : array |
---|
| 229 | a vector of residuals spanning all models, with model |
---|
| 230 | weights applied as appropriate. |
---|
| 231 | degrees_of_freedom : integer |
---|
| 232 | length of the residuals - number of fitted parameters |
---|
| 233 | chisq : float |
---|
| 234 | sum squared residuals; this is not the reduced chisq, which |
---|
| 235 | you can get using chisq/degrees_of_freedom |
---|
| 236 | |
---|
| 237 | These fields are defined for the individual models as well, with |
---|
| 238 | degrees of freedom adjusted to the length of the individual data |
---|
| 239 | set. If the model is not fitted or the weight is zero, the residual |
---|
| 240 | will not be calculated. |
---|
| 241 | |
---|
| 242 | The residuals fields are available only after the model has been |
---|
| 243 | evaluated. |
---|
| 244 | """ |
---|
| 245 | |
---|
| 246 | def __init__(self, models=[]): |
---|
| 247 | """Build an assembly from a list of models.""" |
---|
| 248 | self.parts = [] |
---|
| 249 | for m in models: |
---|
| 250 | self.parts.append(Part(m)) |
---|
| 251 | self._reset() |
---|
| 252 | |
---|
| 253 | def __iter__(self): |
---|
| 254 | """Iterate through the models in order""" |
---|
| 255 | for m in self.parts: yield m |
---|
| 256 | |
---|
| 257 | def __getitem__(self, n): |
---|
| 258 | """Return the nth model""" |
---|
| 259 | return self.parts[n].fitness |
---|
| 260 | |
---|
| 261 | def __setitem__(self, n, fitness): |
---|
| 262 | """Replace the nth model""" |
---|
| 263 | self.parts[n].fitness = fitness |
---|
| 264 | self._reset() |
---|
| 265 | |
---|
| 266 | def __delitem__(self, n): |
---|
| 267 | """Delete the nth model""" |
---|
| 268 | del self.parts[n] |
---|
| 269 | self._reset() |
---|
| 270 | |
---|
| 271 | def weight(self, idx, value=None): |
---|
| 272 | """ |
---|
| 273 | Query the weight on a particular model. |
---|
| 274 | |
---|
| 275 | Set weight to value if value is supplied. |
---|
| 276 | |
---|
| 277 | :Parameters: |
---|
| 278 | idx : integer |
---|
| 279 | model number |
---|
| 280 | value : float |
---|
| 281 | model weight |
---|
| 282 | :return: model weight |
---|
| 283 | """ |
---|
| 284 | if value is not None: |
---|
| 285 | self.parts[idx].weight = value |
---|
| 286 | return self.parts[idx].weight |
---|
| 287 | |
---|
| 288 | def isfitted(self, idx, value=None): |
---|
| 289 | """ |
---|
| 290 | Query if a particular model is fitted. |
---|
| 291 | |
---|
| 292 | Set isfitted to value if value is supplied. |
---|
| 293 | |
---|
| 294 | :param idx: model number |
---|
| 295 | :type idx: integer |
---|
| 296 | :param value: |
---|
| 297 | """ |
---|
| 298 | if value is not None: |
---|
| 299 | self.parts[idx].isfitted = value |
---|
| 300 | return self.parts[idx].isfitted |
---|
| 301 | |
---|
| 302 | def append(self, fitness, weight=1.0, isfitted=True): |
---|
| 303 | """ |
---|
| 304 | Add a model to the end of set. |
---|
| 305 | |
---|
| 306 | :param fitness: the fitting model |
---|
[5ab5cae] | 307 | The fitting model can be an instance of `park.assembly.Fitness`, |
---|
| 308 | or a tuple of (`park.model.Model`,`park.data.Data1D`) |
---|
[3570545] | 309 | :param weight: model weighting (usually 1.0) |
---|
| 310 | :param isfitted: whether model should be fit (equivalent to weight 0.) |
---|
| 311 | """ |
---|
| 312 | self.parts.append(Part(fitness,weight,isfitted)) |
---|
| 313 | self._reset() |
---|
| 314 | |
---|
| 315 | def insert(self, idx, fitness, weight=1.0, isfitted=True): |
---|
| 316 | """Add a model to a particular position in the set.""" |
---|
| 317 | self.parts.insert(idx,Part(fitness,weight,isfitted)) |
---|
| 318 | self._reset() |
---|
| 319 | |
---|
| 320 | def _reset(self): |
---|
| 321 | """Adjust the parameter set after the addition of a new model.""" |
---|
| 322 | subsets = [m.fitness.parameterset for m in self] |
---|
| 323 | self.parameterset = ParameterSet('root',subsets) |
---|
| 324 | self.parameterset.setprefix() |
---|
| 325 | #print [p.path for p in self.parameterset.flatten()] |
---|
| 326 | |
---|
| 327 | def eval(self): |
---|
| 328 | """ |
---|
| 329 | Recalculate the theory functions, and from them, the |
---|
| 330 | residuals and chisq. |
---|
| 331 | |
---|
| 332 | :note: Call this after the parameters have been updated. |
---|
| 333 | """ |
---|
| 334 | # Handle abort from a separate thread. |
---|
| 335 | self._cancel = False |
---|
| 336 | |
---|
| 337 | # Evaluate the computed parameters |
---|
| 338 | self._fitexpression() |
---|
| 339 | |
---|
| 340 | # Check that the resulting parameters are in a feasible region. |
---|
| 341 | if not self.isfeasible(): return numpy.inf |
---|
| 342 | |
---|
| 343 | resid = [] |
---|
| 344 | k = len(self._fitparameters) |
---|
| 345 | for m in self.parts: |
---|
| 346 | # In order to support abort, need to be able to propagate an |
---|
| 347 | # external abort signal from self.abort() into an abort signal |
---|
| 348 | # for the particular model. Can't see a way to do this which |
---|
| 349 | # doesn't involve setting a state variable. |
---|
| 350 | self._current_model = m |
---|
| 351 | if self._cancel: return numpy.inf |
---|
| 352 | if m.isfitted and m.weight != 0: |
---|
| 353 | m.residuals = m.fitness.residuals() |
---|
| 354 | N = len(m.residuals) |
---|
| 355 | m.degrees_of_freedom = N-k if N>k else 1 |
---|
| 356 | m.chisq = numpy.sum(m.residuals**2) |
---|
| 357 | resid.append(m.weight*m.residuals) |
---|
| 358 | self.residuals = numpy.hstack(resid) |
---|
| 359 | N = len(self.residuals) |
---|
| 360 | self.degrees_of_freedom = N-k if N>k else 1 |
---|
| 361 | self.chisq = numpy.sum(self.residuals**2) |
---|
| 362 | return self.chisq |
---|
| 363 | |
---|
| 364 | def jacobian(self, pvec, step=1e-8): |
---|
| 365 | """ |
---|
| 366 | Returns the derivative wrt the fit parameters at point p. |
---|
| 367 | |
---|
| 368 | Numeric derivatives are calculated based on step, where step is |
---|
| 369 | the portion of the total range for parameter j, or the portion of |
---|
| 370 | point value p_j if the range on parameter j is infinite. |
---|
| 371 | """ |
---|
| 372 | # Make sure the input vector is an array |
---|
| 373 | pvec = numpy.asarray(pvec) |
---|
| 374 | # We are being lazy here. We can precompute the bounds, we can |
---|
| 375 | # use the residuals_deriv from the sub-models which have analytic |
---|
| 376 | # derivatives and we need only recompute the models which depend |
---|
| 377 | # on the varying parameters. |
---|
| 378 | # Meanwhile, let's compute the numeric derivative using the |
---|
| 379 | # three point formula. |
---|
| 380 | # We are not checking that the varied parameter in numeric |
---|
| 381 | # differentiation is indeed feasible in the interval of interest. |
---|
| 382 | range = zip(*[p.range for p in self._fitparameters]) |
---|
| 383 | lo,hi = [numpy.asarray(v) for v in range] |
---|
| 384 | delta = (hi-lo)*step |
---|
| 385 | # For infinite ranges, use p*1e-8 for the step size |
---|
| 386 | idx = numpy.isinf(delta) |
---|
| 387 | #print "J",idx,delta,pvec,type(idx),type(delta),type(pvec) |
---|
| 388 | delta[idx] = pvec[idx]*step |
---|
| 389 | delta[delta==0] = step |
---|
| 390 | |
---|
| 391 | # Set the initial value |
---|
| 392 | for k,v in enumerate(pvec): |
---|
| 393 | self._fitparameters[k].value = v |
---|
| 394 | # Gather the residuals |
---|
| 395 | r = [] |
---|
| 396 | for k,v in enumerate(pvec): |
---|
| 397 | # Center point formula: |
---|
| 398 | # df/dv = lim_{h->0} ( f(v+h)-f(v-h) ) / ( 2h ) |
---|
| 399 | h = delta[k] |
---|
| 400 | self._fitparameters[k].value = v + h |
---|
| 401 | self.eval() |
---|
| 402 | rk = self.residuals |
---|
| 403 | self._fitparameters[k].value = v - h |
---|
| 404 | self.eval() |
---|
| 405 | rk -= self.residuals |
---|
| 406 | self._fitparameters[k].value = v |
---|
| 407 | r.append(rk/(2*h)) |
---|
| 408 | # return the jacobian |
---|
| 409 | return numpy.vstack(r).T |
---|
| 410 | |
---|
| 411 | |
---|
| 412 | def cov(self, pvec): |
---|
| 413 | """ |
---|
| 414 | Return the covariance matrix inv(J'J) at point p. |
---|
| 415 | """ |
---|
| 416 | |
---|
| 417 | # Find cov of f at p |
---|
| 418 | # cov(f,p) = inv(J'J) |
---|
| 419 | # Use SVD |
---|
| 420 | # J = U S V' |
---|
| 421 | # J'J = (U S V')' (U S V') |
---|
| 422 | # = V S' U' U S V' |
---|
| 423 | # = V S S V' |
---|
| 424 | # inv(J'J) = inv(V S S V') |
---|
| 425 | # = inv(V') inv(S S) inv(V) |
---|
| 426 | # = V inv (S S) V' |
---|
| 427 | J = self.jacobian(pvec) |
---|
| 428 | u,s,vh = numpy.linalg.svd(J,0) |
---|
| 429 | JTJinv = numpy.dot(vh.T.conj()/s**2,vh) |
---|
| 430 | return JTJinv |
---|
| 431 | |
---|
| 432 | def stderr(self, pvec): |
---|
| 433 | """ |
---|
| 434 | Return parameter uncertainty. |
---|
| 435 | |
---|
| 436 | This is just the sqrt diagonal of covariance matrix inv(J'J) at point p. |
---|
| 437 | """ |
---|
| 438 | return numpy.sqrt(numpy.diag(self.cov(pvec))) |
---|
| 439 | |
---|
| 440 | def isfeasible(self): |
---|
| 441 | """ |
---|
| 442 | Returns true if the parameter set is in a feasible region of the |
---|
| 443 | modeling space. |
---|
| 444 | """ |
---|
| 445 | return True |
---|
| 446 | |
---|
| 447 | # Fitting service interface |
---|
| 448 | def fit_parameters(self): |
---|
| 449 | """ |
---|
| 450 | Return an alphabetical list of the fitting parameters. |
---|
| 451 | |
---|
| 452 | This function is called once at the beginning of a fit, |
---|
| 453 | and serves as a convenient place to precalculate what |
---|
| 454 | can be precalculated such as the set of fitting parameters |
---|
| 455 | and the parameter expressions evaluator. |
---|
| 456 | """ |
---|
| 457 | self.parameterset.setprefix() |
---|
| 458 | self._fitparameters = self.parameterset.fitted |
---|
| 459 | self._restraints = self.parameterset.restrained |
---|
| 460 | pars = self.parameterset.flatten() |
---|
| 461 | context = self.parameterset.gather_context() |
---|
| 462 | self._fitexpression = park.expression.build_eval(pars,context) |
---|
| 463 | #print "constraints",self._fitexpression.__doc__ |
---|
| 464 | |
---|
| 465 | self._fitparameters.sort(lambda a,b: cmp(a.path,b.path)) |
---|
| 466 | # Convert to fitparameter a object |
---|
| 467 | fitpars = [FitParameter(p.path,p.range,p.value) |
---|
| 468 | for p in self._fitparameters] |
---|
| 469 | return fitpars |
---|
| 470 | |
---|
| 471 | def set_result(self, result): |
---|
| 472 | """ |
---|
| 473 | Set the parameters resulting from the fit into the parameter set, |
---|
| 474 | and update the calculated expression. |
---|
| 475 | |
---|
| 476 | The parameter values may be retrieved by walking the assembly.parameterset |
---|
| 477 | tree, checking each parameter for isfitted, iscomputed, or isfixed. |
---|
| 478 | For example:: |
---|
| 479 | |
---|
| 480 | assembly.set_result(result) |
---|
| 481 | for p in assembly.parameterset.flatten(): |
---|
| 482 | if p.isfitted(): |
---|
| 483 | print "%s %g in [%g,%g]"%(p.path,p.value,p.range[0],p.range[1]) |
---|
| 484 | elif p.iscomputed(): |
---|
| 485 | print "%s computed as %g"%(p.path.p.value) |
---|
| 486 | |
---|
| 487 | This does not calculate the function or the residuals for these parameters. |
---|
| 488 | You can call assembly.eval() to do this. The residuals will be set in |
---|
| 489 | assembly[i].residuals. The theory and data are model specific, and can |
---|
| 490 | be found in assembly[i].fitness.data. |
---|
| 491 | """ |
---|
| 492 | for n,p in enumerate(result.parameters): |
---|
| 493 | self._fitparameters[n] = p.value |
---|
| 494 | self._fitexpression() |
---|
| 495 | |
---|
| 496 | def all_results(self, result): |
---|
| 497 | """ |
---|
| 498 | Extend result from the fit with the calculated parameters. |
---|
| 499 | """ |
---|
| 500 | calcpars = [FitParameter(p.path,p.range,p.value) |
---|
| 501 | for p in self.parameterset.computed] |
---|
| 502 | result.parameters += calcpars |
---|
| 503 | |
---|
| 504 | def result(self, status='step'): |
---|
| 505 | """ |
---|
| 506 | Details to send back to the fitting client on an improved fit. |
---|
| 507 | |
---|
| 508 | status is 'start', 'step' or 'end' depending if this is the |
---|
| 509 | first result to return, an improved result, or the final result. |
---|
| 510 | |
---|
| 511 | [Not implemented] |
---|
| 512 | """ |
---|
| 513 | return None |
---|
| 514 | |
---|
| 515 | def fresiduals(self, pvec): |
---|
| 516 | chisq = self.__call__(pvec) |
---|
| 517 | return self.residuals |
---|
| 518 | |
---|
| 519 | def __call__(self, pvec): |
---|
| 520 | """ |
---|
| 521 | Cost function. |
---|
| 522 | |
---|
| 523 | Evaluate the system for the parameter vector pvec, returning chisq |
---|
| 524 | as the cost function to be minimized. |
---|
| 525 | |
---|
| 526 | Raises a runtime error if the number of fit parameters is |
---|
| 527 | different than the length of the vector. |
---|
| 528 | """ |
---|
| 529 | # Plug fit parameters into model |
---|
| 530 | #print "Trying",pvec |
---|
| 531 | pars = self._fitparameters |
---|
| 532 | if len(pvec) != len(pars): |
---|
| 533 | raise RuntimeError("Unexpected number of parameters") |
---|
| 534 | for n,value in enumerate(pvec): |
---|
| 535 | pars[n].value = value |
---|
| 536 | # Evaluate model |
---|
| 537 | chisq = self.eval() |
---|
| 538 | # Evaluate additional restraints based on parameter value |
---|
| 539 | # likelihood |
---|
| 540 | restraints_penalty = 0 |
---|
| 541 | for p in self._restraints: |
---|
| 542 | restraints_penalty += p.likelihood(p.value) |
---|
| 543 | # Return total cost function |
---|
| 544 | return self.chisq + restraints_penalty |
---|
| 545 | |
---|
| 546 | def abort(self): |
---|
| 547 | """ |
---|
| 548 | Interrupt the current function evaluation. |
---|
| 549 | |
---|
| 550 | Forward this to the currently executing model if possible. |
---|
| 551 | """ |
---|
| 552 | self._cancel = True |
---|
| 553 | if hasattr(self._current_model,'abort'): |
---|
| 554 | self._current_model.abort() |
---|
| 555 | |
---|
| 556 | class _Exp(park.Model): |
---|
| 557 | """ |
---|
| 558 | Sample model for testing assembly. |
---|
| 559 | """ |
---|
| 560 | parameters = ['a','c'] |
---|
| 561 | def eval(self,x): |
---|
| 562 | return self.a*numpy.exp(self.c*x) |
---|
| 563 | class _Linear(park.Model): |
---|
| 564 | parameters = ['a','c'] |
---|
| 565 | def eval(self,x): |
---|
| 566 | #print "eval",self.a,self.c,x,self.a*x+self.c |
---|
| 567 | return self.a*x+self.c |
---|
| 568 | def example(): |
---|
| 569 | """ |
---|
| 570 | Return an example assembly consisting of a pair of functions, |
---|
| 571 | M1.a*exp(M1.c*x), M2.a*exp(2*M1.c*x) |
---|
| 572 | and ideal data for |
---|
| 573 | M1.a=1, M1.c=1.5, M2.a=2.5 |
---|
| 574 | """ |
---|
| 575 | import numpy |
---|
| 576 | import park |
---|
| 577 | from numpy import inf |
---|
| 578 | # Make some fake data |
---|
| 579 | x1 = numpy.linspace(0,1,11) |
---|
| 580 | x2 = numpy.linspace(0,1,12) |
---|
| 581 | # Define a shared model |
---|
| 582 | if True: # Exp model |
---|
| 583 | y1,y2 = numpy.exp(1.5*x1),2.5*numpy.exp(3*x2) |
---|
| 584 | M1 = _Exp('M1',a=[1,3],c=[1,3]) |
---|
| 585 | M2 = _Exp('M2',a=[1,3],c='2*M1.c') |
---|
| 586 | #M2 = _Exp('M2',a=[1,3],c=3) |
---|
| 587 | else: # Linear model |
---|
| 588 | y1,y2 = x1+1.5, 2.5*x2+3 |
---|
| 589 | M1 = _Linear('M1',a=[1,3],c=[1,3]) |
---|
| 590 | M2 = _Linear('M2',a=[1,3],c='2*M1.c') |
---|
| 591 | if False: # Unbounded |
---|
| 592 | M1.a = [-inf,inf] |
---|
| 593 | M1.c = [-inf,inf] |
---|
| 594 | M2.a = [-inf,inf] |
---|
| 595 | D1 = park.Data1D(x=x1, y=y1) |
---|
| 596 | D2 = park.Data1D(x=x2, y=y2) |
---|
| 597 | # Construct the assembly |
---|
| 598 | assembly = park.Assembly([(M1,D1),(M2,D2)]) |
---|
| 599 | return assembly |
---|
| 600 | |
---|
| 601 | class _Sphere(park.Model): |
---|
| 602 | parameters = ['a','b','c','d','e'] |
---|
| 603 | def eval(self,x): |
---|
| 604 | return self.a*x**2+self.b*x+self.c + exp(self.d) - 3*sin(self.e) |
---|
| 605 | |
---|
| 606 | def example5(): |
---|
| 607 | import numpy |
---|
| 608 | import park |
---|
| 609 | from numpy import inf |
---|
| 610 | # Make some fake data |
---|
| 611 | x = numpy.linspace(0,1,11) |
---|
| 612 | # Define a shared model |
---|
| 613 | S = _Sphere(a=1,b=2,c=3,d=4,e=5) |
---|
| 614 | y = S.eval(x1) |
---|
| 615 | Sfit = _Sphere(a=[-inf,inf],b=[-inf,inf],c=[-inf,inf],d=[-inf,inf],e=[-inf,inf]) |
---|
| 616 | D = park.Data1D(x=x, y=y) |
---|
| 617 | # Construct the assembly |
---|
| 618 | assembly = park.Assembly([(Sfit,D)]) |
---|
| 619 | return assembly |
---|
| 620 | |
---|
| 621 | def test(): |
---|
| 622 | assembly = example() |
---|
| 623 | assert assembly[0].parameterset.name == 'M1' |
---|
| 624 | |
---|
| 625 | # extract the fitting parameters |
---|
| 626 | pars = [p.name for p in assembly.fit_parameters()] |
---|
| 627 | assert set(pars) == set(['M1.a','M1.c','M2.a']) |
---|
| 628 | # Compute chisq and verify constraints are updated properly |
---|
| 629 | assert assembly([1,1.5,2.5]) == 0 |
---|
| 630 | assert assembly[0].model.c == 1.5 and assembly[1].model.c == 3 |
---|
| 631 | |
---|
| 632 | # Try without constraints |
---|
| 633 | assembly[1].set(c=3) |
---|
| 634 | assembly.fit_parameters() # Fit parameters have changed |
---|
| 635 | assert assembly([1,1.5,2.5]) == 0 |
---|
| 636 | |
---|
| 637 | # Check that assembly.cov runs ... still need to check that it is correct! |
---|
| 638 | C = assembly.cov(numpy.array([1,1.5,2.5])) |
---|
| 639 | |
---|
| 640 | if __name__ == "__main__": test() |
---|