Changes in / [065d77d:93cac17] in sasmodels
- Location:
- sasmodels
- Files:
-
- 3 edited
Legend:
- Unmodified
- Added
- Removed
-
sasmodels/mixture.py
rb297ba9 rb2f0e5d 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 parameters 120 # active simultaneously. details.make_details() will throw an error if 121 # too many are used from any one component. 119 122 parameters.max_pd = sum(part.parameters.max_pd for part in parts) 120 123 -
sasmodels/modelinfo.py
ra34b811 r98c045a 70 70 processed.append(parse_parameter(*p)) 71 71 partable = ParameterTable(processed) 72 partable.check_angles( )72 partable.check_angles(strict=True) 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 449 448 self.kernel_parameters = parameters 450 449 self._set_vector_lengths() … … 495 494 self.pd_2d = set(p.name for p in self.call_parameters if p.polydisperse) 496 495 496 # Final checks 497 self.check_duplicates() 498 self.check_angles() 499 497 500 def set_zero_background(self): 498 501 """ … … 506 509 self.defaults = self._get_defaults() 507 510 508 def check_angles(self ):511 def check_angles(self, strict=False): 509 512 """ 510 513 Check that orientation angles are theta, phi and possibly psi. 514 515 *strict* should be True when checking a parameter table defined 516 in a model file, but False when checking from mixture models, etc., 517 where the parameters aren't being passed to a calculator directly. 511 518 """ 512 519 theta = phi = psi = -1 … … 524 531 if p.type != 'orientation': 525 532 raise TypeError("psi must be an orientation parameter") 526 elif p.type == 'orientation' :533 elif p.type == 'orientation' and strict: 527 534 raise TypeError("only theta, phi and psi can be orientation parameters") 528 535 if theta >= 0 and phi >= 0: … … 532 539 if psi >= 0 and psi != phi+1: 533 540 raise TypeError("psi must follow phi") 541 # TODO: Why must theta/phi/psi be at the end? Consistency only? 534 542 if (psi >= 0 and psi != last_par) or (psi < 0 and phi != last_par): 535 raise TypeError("orientation parameters must appear at the " 536 "end of the parameter table") 543 if strict: 544 raise TypeError("orientation parameters must appear at the " 545 "end of the parameter table") 537 546 elif theta >= 0 or phi >= 0 or psi >= 0: 538 547 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 names 552 """ 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)))) 539 562 540 563 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 special 14 parameters that need to be handled in particular ways. Much of the 15 code is used to figure out what special parameters we have, where to 16 find them in the P@S model inputs and how to distribute them to the underlying 17 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 which 21 parameters are polydisperse, the length of the distribution, and where to 22 find it in the data vector. The distributions are ordered from longest to 23 shortest, with length 1 distributions filling out the distribution set. That 24 way the kernel calculator doesn't have to check if it needs another nesting 25 level since it is always there. The data vector consists of a list of target 26 values for the parameters, followed by a concatenation of the distribution 27 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 to 29 details for P and details for S separately, which unfortunately requires 30 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 as 39 :code:`volfraction*scale*P*S + background`. The *scale* and 40 *background* do not show up in the polydispersity structure so 41 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, and 46 *S.volfraction* is elided, otherwise *S.volfraction* is used. If we 47 are using *volfraction* from P we can treat it like all the other P 48 parameters when calling P, but when calling S we need to insert the 49 *P.volfraction* into data vector for S and assign a slot of length 1 50 in the distribution. Because we are using the original layout of the 51 distribution vectors from P@S, but copying it into private data 52 vectors for S and P, we are free to "borrow" a P slots to store the 53 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 of 57 material but S needs the volume fraction enclosed by the shape. The 58 answer is to scale the user specified volume fraction by the form:shell 59 ratio computed from the average form volume and average shell volume 60 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 absolute 62 scaling on the final *I(q)*. The *scale* for P@S should therefore usually 63 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 it 68 may be set to the *radius_effective* value in P@S, depending on 69 *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 effective 71 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 that 75 returns <FF*> and <F><F*>), then *structure_factor_mode* will be added 76 to the P@S parameters right after the S parameters. This mode may be 0 77 for the monodisperse approximation or 1 for the beta approximation. We 78 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 we 80 return *I = scale volfrac/volume ( <FF> + <F>^2 (S-1)) + background*. 81 If not beta then return *I = scale/volume P S + background* . In both 82 cases, return the appropriate immediate values. 83 84 * *radius_effective_mode* 85 If P defines the *radius_effective* function (and therefore 86 *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. Mode 88 will be zero if *radius_effective* is defined by the user using the S 89 parameter; any other value and the *radius_effective* parameter will be 90 filled in from the value computed in P. In the latter case, the 91 polydispersity information for *S.radius_effective* will need to be 92 suppressed, with pd length set to 1, the first value set to the 93 effective radius and the first weight set to 1. Do this after composing 94 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 the 98 start of the data vector (after scale and background). These will be 99 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 can 101 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 both 103 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 with 105 *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 magnetic 109 parameters tacked on to P@S after the regular P and S and after the 110 special *structure_factor_mode* and *radius_effective_mode*. These 111 can be copied as a group after the regular P parameters. There won't 112 be any magnetic S parameters. 113 12 114 """ 13 115 from __future__ import print_function, division … … 84 186 if not s_info.parameters.magnetism_index == []: 85 187 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)) 86 190 p_id, p_name, p_pars = p_info.id, p_info.name, p_info.parameters 87 191 s_id, s_name, s_pars = s_info.id, s_info.name, s_info.parameters 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. 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. 92 199 p_set = set(p.id for p in p_pars.kernel_parameters) 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)) 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 106 207 combined_pars = p_pars.kernel_parameters + s_list + make_extra_pars(p_info) 107 208 parameters = ParameterTable(combined_pars) 108 parameters.max_pd = p_pars.max_pd + s_pars.max_pd 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} 109 220 def random(): 110 221 """Random set of model parameters for product model""" 111 222 combined_pars = p_info.random() 112 s_names = set(par.id for par in s_pars.kernel_parameters) 113 combined_pars.update((translate_name[k], v) 223 combined_pars.update((s_translate[k], v) 114 224 for k, v in s_info.random().items() 115 if k in s_ names)225 if k in s_translate) 116 226 return combined_pars 117 227 … … 173 283 174 284 def _intermediates( 175 F 1,# type: np.ndarray176 F 2,# type: np.ndarray285 F, # type: np.ndarray 286 Fsq, # type: np.ndarray 177 287 S, # type: np.ndarray 178 288 scale, # type: float … … 190 300 # TODO: 2. consider implications if there are intermediate results in P(Q) 191 301 parts = OrderedDict(( 192 ("P(Q)", scale*F 2),302 ("P(Q)", scale*Fsq), 193 303 ("S(Q)", S), 194 ("beta(Q)", F 1**2 / F2),195 ("S_eff(Q)", 1 + (F 1**2 / F2)*(S-1)),304 ("beta(Q)", F**2 / Fsq), 305 ("S_eff(Q)", 1 + (F**2 / Fsq)*(S-1)), 196 306 ("effective_radius", radius_effective), 197 307 ("radius_effective", radius_effective), 198 # ("I(Q)", scale*(F 2 + (F1**2)*(S-1)) + bg),308 # ("I(Q)", scale*(Fsq + (F**2)*(S-1)) + bg), 199 309 )) 200 310 else: 201 311 parts = OrderedDict(( 202 ("P(Q)", scale*F 2),312 ("P(Q)", scale*Fsq), 203 313 ("S(Q)", S), 204 314 ("effective_radius", radius_effective), … … 264 374 self.results = [] # type: List[np.ndarray] 265 375 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)) 383 384 p_info, s_info = self.info.composition[1] 385 p_npars = p_info.parameters.npars 386 s_npars = s_info.parameters.npars 387 388 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 266 431 def Iq(self, call_details, values, cutoff, magnetic): 267 432 # type: (CallDetails, np.ndarray, float, bool) -> np.ndarray 268 269 433 p_info, s_info = self.info.composition[1] 270 p_npars = p_info.parameters.npars 271 p_length = call_details.length[:p_npars] 272 p_offset = call_details.offset[:p_npars] 273 s_npars = s_info.parameters.npars 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 278 have_beta_mode = p_info.have_Fq 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] 434 435 # Retrieve values from the data vector 296 436 scale, background = values[0], values[1] 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 = [] 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 306 443 nvalues = self.info.parameters.nvalues 307 444 nweights = call_details.num_weights 308 445 weights = values[nvalues:nvalues + 2*nweights] 309 446 447 # Can't do 2d and beta_mode just yet 448 if beta_mode and self.p_kernel.dim == '2d': 449 raise NotImplementedError("beta not yet supported for 2D") 450 310 451 # 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] 311 454 p_details = make_details(p_info, p_length, p_offset, nweights) 312 455 p_values = [ 313 456 [1., 0.], # scale=1, background=0, 314 values[ 2:2+p_npars],315 magnetism,457 values[self._p_value_slice], 458 values[self._magentic_slice], 316 459 weights] 317 460 spacer = (32 - sum(len(v) for v in p_values)%32)%32 … … 319 462 p_values = np.hstack(p_values).astype(self.p_kernel.dtype) 320 463 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 GPU 470 321 471 # Construct the calling parameters for S. 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 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 325 483 s_length[0] = 1 326 484 s_details = make_details(s_info, s_length, s_offset, nweights) 327 485 s_values = [ 328 [1., 0.], # scale=1, background=0, 329 values[2+p_npars:2+p_npars+s_npars], 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], 330 492 weights, 331 493 ] … … 334 496 s_values = np.hstack(s_values).astype(self.s_kernel.dtype) 335 497 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.342 498 # Plug R_eff from the form factor into structure factor parameters 343 499 # and scale volume fraction by form:shell volume ratio. These changes … … 347 503 #print("R_eff=%d:%g, volfrac=%g, volume ratio=%g" 348 504 # % (radius_type, radius_effective, volfrac, volume_ratio)) 349 if radius_type > 0: 505 s_dist = s_values[self._s_dist_slice] 506 if er_mode > 0: 350 507 # set the value to the model R_eff and set the weight to 1 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 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. 354 514 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) 518 519 # 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 355 522 356 523 # 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 361 362 # Combine form factor and structure factor 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 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 366 531 367 532 # Capture intermediate values so user can see them. These are … … 375 540 # the results directly rather than through a lazy evaluator. 376 541 self.results = lambda: _intermediates( 377 F 1, F2, S, combined_scale, radius_effective, beta_mode)542 F, Fsq, S, combined_scale, radius_effective, beta_mode) 378 543 379 544 return final_result
Note: See TracChangeset
for help on using the changeset viewer.