Changeset b2f0e5d in sasmodels
- Timestamp:
- Mar 29, 2019 10:14:32 AM (6 years ago)
- Branches:
- ticket-1257-vesicle-product
- Children:
- 98c045a
- Parents:
- 830cf6b
- 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 rb2f0e5d 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() … … 459 458 #self._name_table= dict((p.id, p) for p in parameters) 460 459 460 461 461 # Set the kernel parameters. Assumes background and scale are the 462 462 # first two parameters in the parameter list, but these are not sent … … 495 495 self.pd_2d = set(p.name for p in self.call_parameters if p.polydisperse) 496 496 497 # Final checks 498 self.check_duplicates() 499 self.check_angles() 500 497 501 def set_zero_background(self): 498 502 """ … … 506 510 self.defaults = self._get_defaults() 507 511 508 def check_angles(self ):512 def check_angles(self, strict=False): 509 513 """ 510 514 Check that orientation angles are theta, phi and possibly psi. 515 516 *strict* should be True when checking a parameter table defined 517 in a model file, but False when checking from mixture models, etc., 518 where the parameters aren't being passed to a calculator directly. 511 519 """ 512 520 theta = phi = psi = -1 … … 524 532 if p.type != 'orientation': 525 533 raise TypeError("psi must be an orientation parameter") 526 elif p.type == 'orientation' :534 elif p.type == 'orientation' and strict: 527 535 raise TypeError("only theta, phi and psi can be orientation parameters") 528 536 if theta >= 0 and phi >= 0: … … 532 540 if psi >= 0 and psi != phi+1: 533 541 raise TypeError("psi must follow phi") 542 # TODO: Why must theta/phi/psi be at the end? Consistency only? 534 543 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") 544 if strict: 545 raise TypeError("orientation parameters must appear at the " 546 "end of the parameter table") 537 547 elif theta >= 0 or phi >= 0 or psi >= 0: 538 548 raise TypeError("oriented shapes must have both theta and phi and maybe psi") 549 550 def check_duplicates(self): 551 """ 552 Check for duplicate parameter names 553 """ 554 checked, dups = set(), set() 555 for p in self.call_parameters: 556 if p.id in checked: 557 dups.add(p.id) 558 else: 559 checked.add(p.id) 560 if dups: 561 raise TypeError("Duplicate parameters: {}" 562 .format(", ".join(sorted(dups)))) 539 563 540 564 def __getitem__(self, key): -
sasmodels/product.py
r8795b6f rb2f0e5d 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 … … 189 299 # TODO: 2. consider implications if there are intermediate results in P(Q) 190 300 parts = OrderedDict(( 191 ("P(Q)", scale*F 2),301 ("P(Q)", scale*Fsq), 192 302 ("S(Q)", S), 193 ("beta(Q)", F 1**2 / F2),194 ("S_eff(Q)", 1 + (F 1**2 / F2)*(S-1)),303 ("beta(Q)", F**2 / Fsq), 304 ("S_eff(Q)", 1 + (F**2 / Fsq)*(S-1)), 195 305 ("effective_radius", radius_effective), 196 # ("I(Q)", scale*(F 2 + (F1**2)*(S-1)) + bg),306 # ("I(Q)", scale*(Fsq + (F**2)*(S-1)) + bg), 197 307 )) 198 308 else: 199 309 parts = OrderedDict(( 200 ("P(Q)", scale*F 2),310 ("P(Q)", scale*Fsq), 201 311 ("S(Q)", S), 202 312 ("effective_radius", radius_effective), … … 261 371 self.results = [] # type: List[np.ndarray] 262 372 373 # Find index of volfraction parameter in parameter list 374 for k, p in enumerate(model_info.parameters.call_parameters): 375 if p.id == VOLFRAC_ID: 376 self._volfrac_index = k 377 break 378 else: 379 raise RuntimeError("no %s parameter in %s"%(VOLFRAC_ID, self)) 380 381 p_info, s_info = self.info.composition[1] 382 p_npars = p_info.parameters.npars 383 s_npars = s_info.parameters.npars 384 385 have_beta_mode = p_info.have_Fq 386 have_er_mode = p_info.radius_effective_modes is not None 387 volfrac_in_p = self._volfrac_index < p_npars + 2 # scale & background 388 389 # Slices into the details length/offset structure for P@S. 390 # Made complicated by the possibly missing volfraction in S. 391 self._p_detail_slice = slice(0, p_npars) 392 self._s_detail_slice = slice(p_npars, p_npars+s_npars-volfrac_in_p) 393 self._volfrac_in_p = volfrac_in_p 394 395 # P block from data vector, without scale and background 396 first_p = 2 397 last_p = p_npars + 2 398 self._p_value_slice = slice(first_p, last_p) 399 400 # radius_effective is the first parameter in S from the data vector. 401 self._er_index = last_p 402 403 # S block from data vector, without scale, background, volfrac or er. 404 first_s = last_p + 2 - volfrac_in_p 405 last_s = first_s + s_npars - 2 406 self._s_value_slice = slice(first_s, last_s) 407 408 # S distribution block in S data vector starts after all S values 409 self._s_dist_slice = slice(2 + s_npars, None) 410 411 # structure_factor_mode is the first parameter after P and S. Skip 412 # 2 for scale and background, and subtract 1 in case there is no 413 # volfraction in S. 414 self._beta_mode_index = last_s if have_beta_mode else 0 415 416 # radius_effective_mode is the second parameter after P and S 417 # unless structure_factor_mode isn't available, in which case it 418 # is first. 419 self._er_mode_index = last_s + have_beta_mode if have_er_mode else 0 420 421 # Magnetic parameters are after everything else. If they exist, 422 # they will only be for form factor P, not structure factor S. 423 first_mag = last_s + have_beta_mode + have_er_mode 424 mag_pars = 3*p_info.parameters.nmagnetic 425 last_mag = first_mag + (mag_pars + 3 if mag_pars else 0) 426 self._magentic_slice = slice(first_mag, last_mag) 427 263 428 def Iq(self, call_details, values, cutoff, magnetic): 264 429 # type: (CallDetails, np.ndarray, float, bool) -> np.ndarray 265 266 430 p_info, s_info = self.info.composition[1] 267 p_npars = p_info.parameters.npars 268 p_length = call_details.length[:p_npars] 269 p_offset = call_details.offset[:p_npars] 270 s_npars = s_info.parameters.npars 271 s_length = call_details.length[p_npars:p_npars+s_npars] 272 s_offset = call_details.offset[p_npars:p_npars+s_npars] 273 274 # Beta mode parameter is the first parameter after P and S parameters 275 have_beta_mode = p_info.have_Fq 276 beta_mode_offset = 2+p_npars+s_npars 277 beta_mode = (values[beta_mode_offset] > 0) if have_beta_mode else False 278 if beta_mode and self.p_kernel.dim == '2d': 279 raise NotImplementedError("beta not yet supported for 2D") 280 281 # R_eff type parameter is the second parameter after P and S parameters 282 # unless the model doesn't support beta mode, in which case it is first 283 have_radius_type = p_info.radius_effective_modes is not None 284 #print(p_npars,s_npars) 285 radius_type_offset = 2+p_npars+s_npars + (1 if have_beta_mode else 0) 286 #print(values[radius_type_offset]) 287 radius_type = int(values[radius_type_offset]) if have_radius_type else 0 288 289 # Retrieve the volume fraction, which is the second of the 290 # 'S' parameters in the parameter list, or 2+np in 0-origin, 291 # as well as the scale and background. 292 volfrac = values[3+p_npars] 431 432 # Retrieve values from the data vector 293 433 scale, background = values[0], values[1] 294 295 # if there are magnetic parameters, they will only be on the 296 # form factor P, not the structure factor S. 297 nmagnetic = len(self.info.parameters.magnetism_index) 298 if nmagnetic: 299 spin_index = self.info.parameters.npars + 2 300 magnetism = values[spin_index: spin_index+3+3*nmagnetic] 301 else: 302 magnetism = [] 434 volfrac = values[self._volfrac_index] 435 er_mode = (int(values[self._er_mode_index]) 436 if self._er_mode_index > 0 else 0) 437 beta_mode = (values[self._beta_mode_index] > 0 438 if self._beta_mode_index > 0 else False) 439 303 440 nvalues = self.info.parameters.nvalues 304 441 nweights = call_details.num_weights 305 442 weights = values[nvalues:nvalues + 2*nweights] 306 443 444 # Can't do 2d and beta_mode just yet 445 if beta_mode and self.p_kernel.dim == '2d': 446 raise NotImplementedError("beta not yet supported for 2D") 447 307 448 # Construct the calling parameters for P. 449 p_length = call_details.length[self._p_detail_slice] 450 p_offset = call_details.offset[self._p_detail_slice] 308 451 p_details = make_details(p_info, p_length, p_offset, nweights) 309 452 p_values = [ 310 453 [1., 0.], # scale=1, background=0, 311 values[ 2:2+p_npars],312 magnetism,454 values[self._p_value_slice], 455 values[self._magentic_slice], 313 456 weights] 314 457 spacer = (32 - sum(len(v) for v in p_values)%32)%32 … … 316 459 p_values = np.hstack(p_values).astype(self.p_kernel.dtype) 317 460 461 # Call the form factor kernel to compute <F> and <F^2>. 462 # If the model doesn't support Fq the returned <F> will be None. 463 F, Fsq, radius_effective, shell_volume, volume_ratio \ 464 = self.p_kernel.Fq(p_details, p_values, cutoff, magnetic, er_mode) 465 466 # TODO: async call to the GPU 467 318 468 # Construct the calling parameters for S. 319 if radius_type > 0: 320 # If R_eff comes from form factor, make sure it is monodisperse. 321 # weight is set to 1 later, after the value array is created 469 s_length = call_details.length[self._s_detail_slice] 470 s_offset = call_details.offset[self._s_detail_slice] 471 if self._volfrac_in_p: 472 # Volfrac is in P and missing from S so insert a slot for it. Say 473 # the distribution is length 1 and use the slot for volfraction 474 # from the P distribution. 475 s_length = np.insert(s_length, 1, 1) 476 s_offset = np.insert(s_offset, 1, p_offset[self._volfrac_index - 2]) 477 if er_mode > 0: 478 # If effective_radius comes from P, make sure it is monodisperse. 479 # Weight is set to 1 later, after the value array is created 322 480 s_length[0] = 1 323 481 s_details = make_details(s_info, s_length, s_offset, nweights) 324 482 s_values = [ 325 [1., 0.], # scale=1, background=0, 326 values[2+p_npars:2+p_npars+s_npars], 483 [1., # scale=1 484 0., # background=0, 485 values[self._er_index], # S.radius_effective; may be replaced by P 486 0.], # volfraction; will be replaced by volfrac * volume_ratio 487 # followed by S parameters after effective_radius and volfraction 488 values[self._s_value_slice], 327 489 weights, 328 490 ] … … 331 493 s_values = np.hstack(s_values).astype(self.s_kernel.dtype) 332 494 333 # Call the form factor kernel to compute <F> and <F^2>.334 # If the model doesn't support Fq the returned <F> will be None.335 F1, F2, radius_effective, shell_volume, volume_ratio = self.p_kernel.Fq(336 p_details, p_values, cutoff, magnetic, radius_type)337 338 # Call the structure factor kernel to compute S.339 495 # Plug R_eff from the form factor into structure factor parameters 340 496 # and scale volume fraction by form:shell volume ratio. These changes … … 344 500 #print("R_eff=%d:%g, volfrac=%g, volume ratio=%g" 345 501 # % (radius_type, radius_effective, volfrac, volume_ratio)) 346 if radius_type > 0: 502 s_dist = s_values[self._s_dist_slice] 503 if er_mode > 0: 347 504 # set the value to the model R_eff and set the weight to 1 348 s_values[2] = s_values[2+s_npars+s_offset[0]] = radius_effective 349 s_values[2+s_npars+s_offset[0]+nweights] = 1.0 350 s_values[3] = s_values[2+s_npars+s_offset[1]] = volfrac*volume_ratio 505 s_values[2] = s_dist[s_offset[0]] = radius_effective 506 s_dist[s_offset[0]+nweights] = 1.0 507 s_values[3] = s_dist[s_offset[1]] = volfrac*volume_ratio 508 s_dist[s_offset[1]+nweights] = 1.0 509 510 # Call the structure factor kernel to compute S. 351 511 S = self.s_kernel.Iq(s_details, s_values, cutoff, False) 512 #print("P", Fsq[:10]) 513 #print("S", S[:10]) 514 #print(radius_effective, volfrac*volume_ratio) 515 516 # Combine form factor and structure factor 517 #print("beta", beta_mode, F, Fsq, S) 518 PS = Fsq + F**2*(S-1) if beta_mode else Fsq*S 352 519 353 520 # Determine overall scale factor. Hollow shapes are weighted by 354 # shell_volume, so that is needed for volume normalization. For 355 # solid shapes we can use shell_volume as well since it is equal 356 # to form volume. 357 combined_scale = scale*volfrac/shell_volume 358 359 # Combine form factor and structure factor 360 #print("beta", beta_mode, F1, F2, S) 361 PS = F2 + F1**2*(S-1) if beta_mode else F2*S 362 final_result = combined_scale*PS + background 521 # shell_volume, so that is needed for number density estimation. 522 # For solid shapes we can use shell_volume as well since it is 523 # equal to form volume. If P already has a volfraction parameter, 524 # then assume that it is already on absolute scale, and don't 525 # include volfrac in the combined_scale. 526 combined_scale = scale*(volfrac if not self._volfrac_in_p else 1.0) 527 final_result = combined_scale/shell_volume*PS + background 363 528 364 529 # Capture intermediate values so user can see them. These are … … 372 537 # the results directly rather than through a lazy evaluator. 373 538 self.results = lambda: _intermediates( 374 F 1, F2, S, combined_scale, radius_effective, beta_mode)539 F, Fsq, S, combined_scale, radius_effective, beta_mode) 375 540 376 541 return final_result
Note: See TracChangeset
for help on using the changeset viewer.