Changes in / [93cac17:065d77d] in sasmodels
- Location:
- sasmodels
- Files:
-
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
sasmodels/mixture.py
rb2f0e5d rb297ba9 117 117 combined_pars.append(p) 118 118 parameters = ParameterTable(combined_pars) 119 # Allow for the scenario in which each component has all its PD parameters120 # active simultaneously. details.make_details() will throw an error if121 # too many are used from any one component.122 119 parameters.max_pd = sum(part.parameters.max_pd for part in parts) 123 120 -
sasmodels/modelinfo.py
r98c045a ra34b811 70 70 processed.append(parse_parameter(*p)) 71 71 partable = ParameterTable(processed) 72 partable.check_angles( strict=True)72 partable.check_angles() 73 73 return partable 74 74 … … 446 446 # properties, such as default=0.0 for structure factor backgrounds. 447 447 self.common_parameters = [Parameter(*p) for p in COMMON_PARAMETERS] 448 448 449 self.kernel_parameters = parameters 449 450 self._set_vector_lengths() … … 494 495 self.pd_2d = set(p.name for p in self.call_parameters if p.polydisperse) 495 496 496 # Final checks497 self.check_duplicates()498 self.check_angles()499 500 497 def set_zero_background(self): 501 498 """ … … 509 506 self.defaults = self._get_defaults() 510 507 511 def check_angles(self , strict=False):508 def check_angles(self): 512 509 """ 513 510 Check that orientation angles are theta, phi and possibly psi. 514 515 *strict* should be True when checking a parameter table defined516 in a model file, but False when checking from mixture models, etc.,517 where the parameters aren't being passed to a calculator directly.518 511 """ 519 512 theta = phi = psi = -1 … … 531 524 if p.type != 'orientation': 532 525 raise TypeError("psi must be an orientation parameter") 533 elif p.type == 'orientation' and strict:526 elif p.type == 'orientation': 534 527 raise TypeError("only theta, phi and psi can be orientation parameters") 535 528 if theta >= 0 and phi >= 0: … … 539 532 if psi >= 0 and psi != phi+1: 540 533 raise TypeError("psi must follow phi") 541 # TODO: Why must theta/phi/psi be at the end? Consistency only?542 534 if (psi >= 0 and psi != last_par) or (psi < 0 and phi != last_par): 543 if strict: 544 raise TypeError("orientation parameters must appear at the " 545 "end of the parameter table") 535 raise TypeError("orientation parameters must appear at the " 536 "end of the parameter table") 546 537 elif theta >= 0 or phi >= 0 or psi >= 0: 547 538 raise TypeError("oriented shapes must have both theta and phi and maybe psi") 548 549 def check_duplicates(self):550 """551 Check for duplicate parameter names552 """553 checked, dups = set(), set()554 for p in self.call_parameters:555 if p.id in checked:556 dups.add(p.id)557 else:558 checked.add(p.id)559 if dups:560 raise TypeError("Duplicate parameters: {}"561 .format(", ".join(sorted(dups))))562 539 563 540 def __getitem__(self, key): -
sasmodels/product.py
r065d77d r065d77d 10 10 To use it, first load form factor P and structure factor S, then create 11 11 *make_product_info(P, S)*. 12 13 The P@S models is somewhat complicated because there are many special14 parameters that need to be handled in particular ways. Much of the15 code is used to figure out what special parameters we have, where to16 find them in the P@S model inputs and how to distribute them to the underlying17 P and S model calculators.18 19 The parameter packet received by the P@S is a :class:`details.CallDetails`20 structure along with a data vector. The CallDetails structure indicates which21 parameters are polydisperse, the length of the distribution, and where to22 find it in the data vector. The distributions are ordered from longest to23 shortest, with length 1 distributions filling out the distribution set. That24 way the kernel calculator doesn't have to check if it needs another nesting25 level since it is always there. The data vector consists of a list of target26 values for the parameters, followed by a concatenation of the distribution27 values, and then followed by a concatenation of the distribution weights.28 Given the combined details and data for P@S, we must decompose them in to29 details for P and details for S separately, which unfortunately requires30 intimate knowledge of the data structures and tricky code.31 32 The special parameters are:33 34 * *scale* and *background*:35 First two parameters of the value list in each of P, S and P@S.36 When decomposing P@S parameters, ignore *scale* and *background*,37 instead using 1 and 0 for the first two slots of both P and S.38 After calling P and S individually, the results are combined as39 :code:`volfraction*scale*P*S + background`. The *scale* and40 *background* do not show up in the polydispersity structure so41 they are easy to handle.42 43 * *volfraction*:44 Always the first parameter of S, but it may also be in P. If it is in P,45 then *P.volfraction* is used in the combined P@S list, and46 *S.volfraction* is elided, otherwise *S.volfraction* is used. If we47 are using *volfraction* from P we can treat it like all the other P48 parameters when calling P, but when calling S we need to insert the49 *P.volfraction* into data vector for S and assign a slot of length 150 in the distribution. Because we are using the original layout of the51 distribution vectors from P@S, but copying it into private data52 vectors for S and P, we are free to "borrow" a P slots to store the53 missing *S.volfraction* distribution. We use the *P.volfraction*54 slot itself but any slot will work.55 56 For hollow shapes, *volfraction* represents the volume fraction of57 material but S needs the volume fraction enclosed by the shape. The58 answer is to scale the user specified volume fraction by the form:shell59 ratio computed from the average form volume and average shell volume60 returned from P. Use the original *volfraction* divided by *shell_volume*61 to compute the number density, and scale P@S by that to get absolute62 scaling on the final *I(q)*. The *scale* for P@S should therefore usually63 be one.64 65 * *radius_effective*:66 Always the second parameter of S and always part of P@S, but never in P.67 The value may be calculated using *P.radius_effective()* or it68 may be set to the *radius_effective* value in P@S, depending on69 *radius_effective_mode*. If part of S, the value may be polydisperse.70 If calculated by P, then it will be the weighted average of effective71 radii computed for the polydisperse shape parameters.72 73 * *structure_factor_mode*74 If P@S supports beta approximation (i.e., if it has the *Fq* function that75 returns <FF*> and <F><F*>), then *structure_factor_mode* will be added76 to the P@S parameters right after the S parameters. This mode may be 077 for the monodisperse approximation or 1 for the beta approximation. We78 will add more values here as we implemented more complicated operations,79 but for now P and S must be computed separately. If beta, then we80 return *I = scale volfrac/volume ( <FF> + <F>^2 (S-1)) + background*.81 If not beta then return *I = scale/volume P S + background* . In both82 cases, return the appropriate immediate values.83 84 * *radius_effective_mode*85 If P defines the *radius_effective* function (and therefore86 *P.info.radius_effective_modes* is a list of effective radius modes),87 then *radius_effective_mode* will be the final parameter in P@S. Mode88 will be zero if *radius_effective* is defined by the user using the S89 parameter; any other value and the *radius_effective* parameter will be90 filled in from the value computed in P. In the latter case, the91 polydispersity information for *S.radius_effective* will need to be92 suppressed, with pd length set to 1, the first value set to the93 effective radius and the first weight set to 1. Do this after composing94 the S data vector so the inputs are left untouched.95 96 * *regular parameters*97 The regular P parameters form a block of length *P.info.npars* at the98 start of the data vector (after scale and background). These will be99 followed by *S.effective_radius*, and *S.volfraction* (if *P.volfraction*100 is absent), and then the regular S parameters. The P and S blocks can101 be copied as a group into the respective P and S data vectors.102 We can copy the distribution value and weight vectors untouched to both103 the P and S data vectors since they are referenced by offset and length.104 We can update the radius_effective slots in the P data vector with105 *P.radius_effective()* if needed.106 107 * *magnetic parameters*108 For each P parameter that is an SLD there will be a set of three magnetic109 parameters tacked on to P@S after the regular P and S and after the110 special *structure_factor_mode* and *radius_effective_mode*. These111 can be copied as a group after the regular P parameters. There won't112 be any magnetic S parameters.113 114 12 """ 115 13 from __future__ import print_function, division … … 186 84 if not s_info.parameters.magnetism_index == []: 187 85 raise TypeError("S should not have SLD parameters") 188 if RADIUS_ID in p_info.parameters:189 raise TypeError("P should not have {}".format(RADIUS_ID))190 86 p_id, p_name, p_pars = p_info.id, p_info.name, p_info.parameters 191 87 s_id, s_name, s_pars = s_info.id, s_info.name, s_info.parameters 192 p_has_volfrac = VOLFRAC_ID in p_info.parameters 193 194 # Create list of parameters for the combined model. If a name in 195 # S overlaps a name in P, tag the S parameter name to distinguish it. 196 # If the tagged name also collides it will be caught by the parameter 197 # table builder. Similarly if any special names are abused. Need the 198 # pairs to create the translation table for random model generation. 88 89 # Create list of parameters for the combined model. If there 90 # are any names in P that overlap with those in S, modify the name in S 91 # to distinguish it. 199 92 p_set = set(p.id for p in p_pars.kernel_parameters) 200 s_pairs = [(par, (_tag_parameter(par) if par.id in p_set else par)) 201 for par in s_pars.kernel_parameters 202 # Note: exclude volfraction from s_list if volfraction in p 203 if par.id != VOLFRAC_ID or not p_has_volfrac] 204 s_list = [pair[0] for pair in s_pairs] 205 206 # Build combined parameter table 93 s_list = [(_tag_parameter(par) if par.id in p_set else par) 94 for par in s_pars.kernel_parameters] 95 # Check if still a collision after renaming. This could happen if for 96 # example S has volfrac and P has both volfrac and volfrac_S. 97 if any(p.id in p_set for p in s_list): 98 raise TypeError("name collision: P has P.name and P.name_S while S has S.name") 99 100 # make sure effective radius is not a polydisperse parameter in product 101 s_list[0] = copy(s_list[0]) 102 s_list[0].polydisperse = False 103 104 translate_name = dict((old.id, new.id) for old, new 105 in zip(s_pars.kernel_parameters, s_list)) 207 106 combined_pars = p_pars.kernel_parameters + s_list + make_extra_pars(p_info) 208 107 parameters = ParameterTable(combined_pars) 209 # Allow for the scenario in which each component has all its PD parameters 210 # active simultaneously. details.make_details() will throw an error if 211 # too many are used from any one component. 212 parameters.Pumax_pd = p_pars.max_pd + s_pars.max_pd 213 214 # TODO: does user-defined polydisperse S.radius_effective make sense? 215 # make sure effective radius is not a polydisperse parameter in product 216 #s_list[0] = copy(s_list[0]) 217 #s_list[0].polydisperse = False 218 219 s_translate = {old.id: new.id for old, new in s_pairs} 108 parameters.max_pd = p_pars.max_pd + s_pars.max_pd 220 109 def random(): 221 110 """Random set of model parameters for product model""" 222 111 combined_pars = p_info.random() 223 combined_pars.update((s_translate[k], v) 112 s_names = set(par.id for par in s_pars.kernel_parameters) 113 combined_pars.update((translate_name[k], v) 224 114 for k, v in s_info.random().items() 225 if k in s_ translate)115 if k in s_names) 226 116 return combined_pars 227 117 … … 283 173 284 174 def _intermediates( 285 F ,# type: np.ndarray286 F sq,# type: np.ndarray175 F1, # type: np.ndarray 176 F2, # type: np.ndarray 287 177 S, # type: np.ndarray 288 178 scale, # type: float … … 300 190 # TODO: 2. consider implications if there are intermediate results in P(Q) 301 191 parts = OrderedDict(( 302 ("P(Q)", scale*F sq),192 ("P(Q)", scale*F2), 303 193 ("S(Q)", S), 304 ("beta(Q)", F **2 / Fsq),305 ("S_eff(Q)", 1 + (F **2 / Fsq)*(S-1)),194 ("beta(Q)", F1**2 / F2), 195 ("S_eff(Q)", 1 + (F1**2 / F2)*(S-1)), 306 196 ("effective_radius", radius_effective), 307 197 ("radius_effective", radius_effective), 308 # ("I(Q)", scale*(F sq + (F**2)*(S-1)) + bg),198 # ("I(Q)", scale*(F2 + (F1**2)*(S-1)) + bg), 309 199 )) 310 200 else: 311 201 parts = OrderedDict(( 312 ("P(Q)", scale*F sq),202 ("P(Q)", scale*F2), 313 203 ("S(Q)", S), 314 204 ("effective_radius", radius_effective), … … 374 264 self.results = [] # type: List[np.ndarray] 375 265 376 # Find index of volfraction parameter in parameter list 377 for k, p in enumerate(model_info.parameters.call_parameters): 378 if p.id == VOLFRAC_ID: 379 self._volfrac_index = k 380 break 381 else: 382 raise RuntimeError("no %s parameter in %s"%(VOLFRAC_ID, self)) 266 def Iq(self, call_details, values, cutoff, magnetic): 267 # type: (CallDetails, np.ndarray, float, bool) -> np.ndarray 383 268 384 269 p_info, s_info = self.info.composition[1] 385 270 p_npars = p_info.parameters.npars 271 p_length = call_details.length[:p_npars] 272 p_offset = call_details.offset[:p_npars] 386 273 s_npars = s_info.parameters.npars 387 274 s_length = call_details.length[p_npars:p_npars+s_npars] 275 s_offset = call_details.offset[p_npars:p_npars+s_npars] 276 277 # Beta mode parameter is the first parameter after P and S parameters 388 278 have_beta_mode = p_info.have_Fq 389 have_er_mode = p_info.radius_effective_modes is not None 390 volfrac_in_p = self._volfrac_index < p_npars + 2 # scale & background 391 392 # Slices into the details length/offset structure for P@S. 393 # Made complicated by the possibly missing volfraction in S. 394 self._p_detail_slice = slice(0, p_npars) 395 self._s_detail_slice = slice(p_npars, p_npars+s_npars-volfrac_in_p) 396 self._volfrac_in_p = volfrac_in_p 397 398 # P block from data vector, without scale and background 399 first_p = 2 400 last_p = p_npars + 2 401 self._p_value_slice = slice(first_p, last_p) 402 403 # radius_effective is the first parameter in S from the data vector. 404 self._er_index = last_p 405 406 # S block from data vector, without scale, background, volfrac or er. 407 first_s = last_p + 2 - volfrac_in_p 408 last_s = first_s + s_npars - 2 409 self._s_value_slice = slice(first_s, last_s) 410 411 # S distribution block in S data vector starts after all S values 412 self._s_dist_slice = slice(2 + s_npars, None) 413 414 # structure_factor_mode is the first parameter after P and S. Skip 415 # 2 for scale and background, and subtract 1 in case there is no 416 # volfraction in S. 417 self._beta_mode_index = last_s if have_beta_mode else 0 418 419 # radius_effective_mode is the second parameter after P and S 420 # unless structure_factor_mode isn't available, in which case it 421 # is first. 422 self._er_mode_index = last_s + have_beta_mode if have_er_mode else 0 423 424 # Magnetic parameters are after everything else. If they exist, 425 # they will only be for form factor P, not structure factor S. 426 first_mag = last_s + have_beta_mode + have_er_mode 427 mag_pars = 3*p_info.parameters.nmagnetic 428 last_mag = first_mag + (mag_pars + 3 if mag_pars else 0) 429 self._magentic_slice = slice(first_mag, last_mag) 430 431 def Iq(self, call_details, values, cutoff, magnetic): 432 # type: (CallDetails, np.ndarray, float, bool) -> np.ndarray 433 p_info, s_info = self.info.composition[1] 434 435 # Retrieve values from the data vector 279 beta_mode_offset = 2+p_npars+s_npars 280 beta_mode = (values[beta_mode_offset] > 0) if have_beta_mode else False 281 if beta_mode and self.p_kernel.dim == '2d': 282 raise NotImplementedError("beta not yet supported for 2D") 283 284 # R_eff type parameter is the second parameter after P and S parameters 285 # unless the model doesn't support beta mode, in which case it is first 286 have_radius_type = p_info.radius_effective_modes is not None 287 #print(p_npars,s_npars) 288 radius_type_offset = 2+p_npars+s_npars + (1 if have_beta_mode else 0) 289 #print(values[radius_type_offset]) 290 radius_type = int(values[radius_type_offset]) if have_radius_type else 0 291 292 # Retrieve the volume fraction, which is the second of the 293 # 'S' parameters in the parameter list, or 2+np in 0-origin, 294 # as well as the scale and background. 295 volfrac = values[3+p_npars] 436 296 scale, background = values[0], values[1] 437 volfrac = values[self._volfrac_index] 438 er_mode = (int(values[self._er_mode_index]) 439 if self._er_mode_index > 0 else 0) 440 beta_mode = (values[self._beta_mode_index] > 0 441 if self._beta_mode_index > 0 else False) 442 297 298 # if there are magnetic parameters, they will only be on the 299 # form factor P, not the structure factor S. 300 nmagnetic = len(self.info.parameters.magnetism_index) 301 if nmagnetic: 302 spin_index = self.info.parameters.npars + 2 303 magnetism = values[spin_index: spin_index+3+3*nmagnetic] 304 else: 305 magnetism = [] 443 306 nvalues = self.info.parameters.nvalues 444 307 nweights = call_details.num_weights 445 308 weights = values[nvalues:nvalues + 2*nweights] 446 309 447 # Can't do 2d and beta_mode just yet448 if beta_mode and self.p_kernel.dim == '2d':449 raise NotImplementedError("beta not yet supported for 2D")450 451 310 # Construct the calling parameters for P. 452 p_length = call_details.length[self._p_detail_slice]453 p_offset = call_details.offset[self._p_detail_slice]454 311 p_details = make_details(p_info, p_length, p_offset, nweights) 455 312 p_values = [ 456 313 [1., 0.], # scale=1, background=0, 457 values[ self._p_value_slice],458 values[self._magentic_slice],314 values[2:2+p_npars], 315 magnetism, 459 316 weights] 460 317 spacer = (32 - sum(len(v) for v in p_values)%32)%32 … … 462 319 p_values = np.hstack(p_values).astype(self.p_kernel.dtype) 463 320 464 # Call the form factor kernel to compute <F> and <F^2>.465 # If the model doesn't support Fq the returned <F> will be None.466 F, Fsq, radius_effective, shell_volume, volume_ratio \467 = self.p_kernel.Fq(p_details, p_values, cutoff, magnetic, er_mode)468 469 # TODO: async call to the GPU470 471 321 # Construct the calling parameters for S. 472 s_length = call_details.length[self._s_detail_slice] 473 s_offset = call_details.offset[self._s_detail_slice] 474 if self._volfrac_in_p: 475 # Volfrac is in P and missing from S so insert a slot for it. Say 476 # the distribution is length 1 and use the slot for volfraction 477 # from the P distribution. 478 s_length = np.insert(s_length, 1, 1) 479 s_offset = np.insert(s_offset, 1, p_offset[self._volfrac_index - 2]) 480 if er_mode > 0: 481 # If effective_radius comes from P, make sure it is monodisperse. 482 # Weight is set to 1 later, after the value array is created 322 if radius_type > 0: 323 # If R_eff comes from form factor, make sure it is monodisperse. 324 # weight is set to 1 later, after the value array is created 483 325 s_length[0] = 1 484 326 s_details = make_details(s_info, s_length, s_offset, nweights) 485 327 s_values = [ 486 [1., # scale=1 487 0., # background=0, 488 values[self._er_index], # S.radius_effective; may be replaced by P 489 0.], # volfraction; will be replaced by volfrac * volume_ratio 490 # followed by S parameters after effective_radius and volfraction 491 values[self._s_value_slice], 328 [1., 0.], # scale=1, background=0, 329 values[2+p_npars:2+p_npars+s_npars], 492 330 weights, 493 331 ] … … 496 334 s_values = np.hstack(s_values).astype(self.s_kernel.dtype) 497 335 336 # Call the form factor kernel to compute <F> and <F^2>. 337 # If the model doesn't support Fq the returned <F> will be None. 338 F1, F2, radius_effective, shell_volume, volume_ratio = self.p_kernel.Fq( 339 p_details, p_values, cutoff, magnetic, radius_type) 340 341 # Call the structure factor kernel to compute S. 498 342 # Plug R_eff from the form factor into structure factor parameters 499 343 # and scale volume fraction by form:shell volume ratio. These changes … … 503 347 #print("R_eff=%d:%g, volfrac=%g, volume ratio=%g" 504 348 # % (radius_type, radius_effective, volfrac, volume_ratio)) 505 s_dist = s_values[self._s_dist_slice] 506 if er_mode > 0: 349 if radius_type > 0: 507 350 # set the value to the model R_eff and set the weight to 1 508 s_values[2] = s_dist[s_offset[0]] = radius_effective 509 s_dist[s_offset[0]+nweights] = 1.0 510 s_values[3] = s_dist[s_offset[1]] = volfrac*volume_ratio 511 s_dist[s_offset[1]+nweights] = 1.0 512 513 # Call the structure factor kernel to compute S. 351 s_values[2] = s_values[2+s_npars+s_offset[0]] = radius_effective 352 s_values[2+s_npars+s_offset[0]+nweights] = 1.0 353 s_values[3] = s_values[2+s_npars+s_offset[1]] = volfrac*volume_ratio 514 354 S = self.s_kernel.Iq(s_details, s_values, cutoff, False) 515 #print("P", Fsq[:10]) 516 #print("S", S[:10]) 517 #print(radius_effective, volfrac*volume_ratio) 355 356 # Determine overall scale factor. Hollow shapes are weighted by 357 # shell_volume, so that is needed for volume normalization. For 358 # solid shapes we can use shell_volume as well since it is equal 359 # to form volume. 360 combined_scale = scale*volfrac/shell_volume 518 361 519 362 # Combine form factor and structure factor 520 #print("beta", beta_mode, F, Fsq, S) 521 PS = Fsq + F**2*(S-1) if beta_mode else Fsq*S 522 523 # Determine overall scale factor. Hollow shapes are weighted by 524 # shell_volume, so that is needed for number density estimation. 525 # For solid shapes we can use shell_volume as well since it is 526 # equal to form volume. If P already has a volfraction parameter, 527 # then assume that it is already on absolute scale, and don't 528 # include volfrac in the combined_scale. 529 combined_scale = scale*(volfrac if not self._volfrac_in_p else 1.0) 530 final_result = combined_scale/shell_volume*PS + background 363 #print("beta", beta_mode, F1, F2, S) 364 PS = F2 + F1**2*(S-1) if beta_mode else F2*S 365 final_result = combined_scale*PS + background 531 366 532 367 # Capture intermediate values so user can see them. These are … … 540 375 # the results directly rather than through a lazy evaluator. 541 376 self.results = lambda: _intermediates( 542 F , Fsq, S, combined_scale, radius_effective, beta_mode)377 F1, F2, S, combined_scale, radius_effective, beta_mode) 543 378 544 379 return final_result
Note: See TracChangeset
for help on using the changeset viewer.