Changeset fd7291e in sasmodels for sasmodels/product.py
- Timestamp:
- Mar 6, 2019 4:05:52 PM (5 years ago)
- Branches:
- master, core_shell_microgels, magnetic_model, ticket-1257-vesicle-product, ticket_1156, ticket_1265_superball, ticket_822_more_unit_tests
- Children:
- c11d09f
- Parents:
- 21c93c3 (diff), 9150036 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent. - File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
sasmodels/product.py
rb171acd rfd7291e 13 13 from __future__ import print_function, division 14 14 15 from collections import OrderedDict 16 15 17 from copy import copy 16 18 import numpy as np # type: ignore 17 19 18 from .modelinfo import ParameterTable, ModelInfo 20 from .modelinfo import ParameterTable, ModelInfo, Parameter, parse_parameter 19 21 from .kernel import KernelModel, Kernel 20 22 from .details import make_details, dispersion_mesh … … 22 24 # pylint: disable=unused-import 23 25 try: 24 from typing import Tuple 26 from typing import Tuple, Callable, Union 25 27 except ImportError: 26 28 pass … … 35 37 #] 36 38 37 ER_ID = "radius_effective" 38 VF_ID = "volfraction" 39 40 # TODO: core_shell_sphere model has suppressed the volume ratio calculation 41 # revert it after making VR and ER available at run time as constraints. 39 STRUCTURE_MODE_ID = "structure_factor_mode" 40 RADIUS_MODE_ID = "radius_effective_mode" 41 RADIUS_ID = "radius_effective" 42 VOLFRAC_ID = "volfraction" 43 def make_extra_pars(p_info): 44 pars = [] 45 if p_info.have_Fq: 46 par = parse_parameter( 47 STRUCTURE_MODE_ID, 48 "", 49 0, 50 [["P*S","P*(1+beta*(S-1))"]], 51 "", 52 "Structure factor calculation") 53 pars.append(par) 54 if p_info.effective_radius_type is not None: 55 par = parse_parameter( 56 RADIUS_MODE_ID, 57 "", 58 1, 59 [["unconstrained"] + p_info.effective_radius_type], 60 "", 61 "Effective radius calculation") 62 pars.append(par) 63 return pars 64 42 65 def make_product_info(p_info, s_info): 43 66 # type: (ModelInfo, ModelInfo) -> ModelInfo … … 50 73 # have any magnetic parameters 51 74 if not len(s_info.parameters.kernel_parameters) >= 2: 52 raise TypeError("S needs {} and {} as its first parameters".format( ER_ID, VF_ID))53 if not s_info.parameters.kernel_parameters[0].id == ER_ID:54 raise TypeError("S needs {} as first parameter".format( ER_ID))55 if not s_info.parameters.kernel_parameters[1].id == V F_ID:56 raise TypeError("S needs {} as second parameter".format(V F_ID))75 raise TypeError("S needs {} and {} as its first parameters".format(RADIUS_ID, VOLFRAC_ID)) 76 if not s_info.parameters.kernel_parameters[0].id == RADIUS_ID: 77 raise TypeError("S needs {} as first parameter".format(RADIUS_ID)) 78 if not s_info.parameters.kernel_parameters[1].id == VOLFRAC_ID: 79 raise TypeError("S needs {} as second parameter".format(VOLFRAC_ID)) 57 80 if not s_info.parameters.magnetism_index == []: 58 81 raise TypeError("S should not have SLD parameters") … … 60 83 s_id, s_name, s_pars = s_info.id, s_info.name, s_info.parameters 61 84 62 # Create list of parameters for the combined model. Skip the first 63 # parameter of S, which we verified above is effective radius. If there 85 # Create list of parameters for the combined model. If there 64 86 # are any names in P that overlap with those in S, modify the name in S 65 87 # to distinguish it. 66 88 p_set = set(p.id for p in p_pars.kernel_parameters) 67 89 s_list = [(_tag_parameter(par) if par.id in p_set else par) 68 for par in s_pars.kernel_parameters [1:]]90 for par in s_pars.kernel_parameters] 69 91 # Check if still a collision after renaming. This could happen if for 70 92 # example S has volfrac and P has both volfrac and volfrac_S. … … 72 94 raise TypeError("name collision: P has P.name and P.name_S while S has S.name") 73 95 96 # make sure effective radius is not a polydisperse parameter in product 97 s_list[0] = copy(s_list[0]) 98 s_list[0].polydisperse = False 99 74 100 translate_name = dict((old.id, new.id) for old, new 75 in zip(s_pars.kernel_parameters [1:], s_list))76 combined_pars = p_pars.kernel_parameters + s_list 101 in zip(s_pars.kernel_parameters, s_list)) 102 combined_pars = p_pars.kernel_parameters + s_list + make_extra_pars(p_info) 77 103 parameters = ParameterTable(combined_pars) 78 104 parameters.max_pd = p_pars.max_pd + s_pars.max_pd 79 105 def random(): 80 106 combined_pars = p_info.random() 81 s_names = set(par.id for par in s_pars.kernel_parameters [1:])107 s_names = set(par.id for par in s_pars.kernel_parameters) 82 108 combined_pars.update((translate_name[k], v) 83 109 for k, v in s_info.random().items() … … 100 126 #model_info.tests = [] 101 127 #model_info.source = [] 102 # Iq, Iqxy, form_volume, ER, VR and sesans103 128 # Remember the component info blocks so we can build the model 104 129 model_info.composition = ('product', [p_info, s_info]) … … 141 166 return par 142 167 168 def _intermediates( 169 F1, # type: np.ndarray 170 F2, # type: np.ndarray 171 S, # type: np.ndarray 172 scale, # type: float 173 effective_radius, # type: float 174 beta_mode, # type: bool 175 ): 176 # type: (...) -> OrderedDict[str, Union[np.ndarray, float]] 177 """ 178 Returns intermediate results for beta approximation-enabled product. 179 The result may be an array or a float. 180 """ 181 if beta_mode: 182 # TODO: 1. include calculated Q vector 183 # TODO: 2. consider implications if there are intermediate results in P(Q) 184 parts = OrderedDict(( 185 ("P(Q)", scale*F2), 186 ("S(Q)", S), 187 ("beta(Q)", F1**2 / F2), 188 ("S_eff(Q)", 1 + (F1**2 / F2)*(S-1)), 189 ("effective_radius", effective_radius), 190 # ("I(Q)", scale*(F2 + (F1**2)*(S-1)) + bg), 191 )) 192 else: 193 parts = OrderedDict(( 194 ("P(Q)", scale*F2), 195 ("S(Q)", S), 196 ("effective_radius", effective_radius), 197 )) 198 return parts 199 143 200 class ProductModel(KernelModel): 144 201 def __init__(self, model_info, P, S): … … 150 207 #: Structure factor modelling interaction between particles. 151 208 self.S = S 209 152 210 #: Model precision. This is not really relevant, since it is the 153 211 #: individual P and S models that control the effective dtype, … … 167 225 # in opencl; or both in opencl, but one in single precision and the 168 226 # other in double precision). 227 169 228 p_kernel = self.P.make_kernel(q_vectors) 170 229 s_kernel = self.S.make_kernel(q_vectors) … … 191 250 def __call__(self, call_details, values, cutoff, magnetic): 192 251 # type: (CallDetails, np.ndarray, float, bool) -> np.ndarray 252 193 253 p_info, s_info = self.info.composition[1] 254 p_npars = p_info.parameters.npars 255 p_length = call_details.length[:p_npars] 256 p_offset = call_details.offset[:p_npars] 257 s_npars = s_info.parameters.npars 258 s_length = call_details.length[p_npars:p_npars+s_npars] 259 s_offset = call_details.offset[p_npars:p_npars+s_npars] 260 261 # Beta mode parameter is the first parameter after P and S parameters 262 have_beta_mode = p_info.have_Fq 263 beta_mode_offset = 2+p_npars+s_npars 264 beta_mode = (values[beta_mode_offset] > 0) if have_beta_mode else False 265 if beta_mode and self.p_kernel.dim== '2d': 266 raise NotImplementedError("beta not yet supported for 2D") 267 268 # R_eff type parameter is the second parameter after P and S parameters 269 # unless the model doesn't support beta mode, in which case it is first 270 have_radius_type = p_info.effective_radius_type is not None 271 radius_type_offset = 2+p_npars+s_npars + (1 if have_beta_mode else 0) 272 radius_type = int(values[radius_type_offset]) if have_radius_type else 0 273 274 # Retrieve the volume fraction, which is the second of the 275 # 'S' parameters in the parameter list, or 2+np in 0-origin, 276 # as well as the scale and background. 277 volfrac = values[3+p_npars] 278 scale, background = values[0], values[1] 194 279 195 280 # if there are magnetic parameters, they will only be on the … … 206 291 207 292 # Construct the calling parameters for P. 208 p_npars = p_info.parameters.npars209 p_length = call_details.length[:p_npars]210 p_offset = call_details.offset[:p_npars]211 293 p_details = make_details(p_info, p_length, p_offset, nweights) 212 # Set p scale to the volume fraction in s, which is the first of the 213 # 'S' parameters in the parameter list, or 2+np in 0-origin. 214 volfrac = values[2+p_npars] 215 p_values = [[volfrac, 0.0], values[2:2+p_npars], magnetism, weights] 294 p_values = [ 295 [1., 0.], # scale=1, background=0, 296 values[2:2+p_npars], 297 magnetism, 298 weights] 216 299 spacer = (32 - sum(len(v) for v in p_values)%32)%32 217 300 p_values.append([0.]*spacer) 218 301 p_values = np.hstack(p_values).astype(self.p_kernel.dtype) 219 302 220 # Call ER and VR for P since these are needed for S.221 p_er, p_vr = calc_er_vr(p_info, p_details, p_values)222 s_vr = (volfrac/p_vr if p_vr != 0. else volfrac)223 #print("volfrac:%g p_er:%g p_vr:%g s_vr:%g"%(volfrac,p_er,p_vr,s_vr))224 225 303 # Construct the calling parameters for S. 226 # The effective radius is not in the combined parameter list, so 227 # the number of 'S' parameters is one less than expected. The 228 # computed effective radius needs to be added into the weights 229 # vector, especially since it is a polydisperse parameter in the 230 # stand-alone structure factor models. We will added it at the 231 # end so the remaining offsets don't need to change. 232 s_npars = s_info.parameters.npars-1 233 s_length = call_details.length[p_npars:p_npars+s_npars] 234 s_offset = call_details.offset[p_npars:p_npars+s_npars] 235 s_length = np.hstack((1, s_length)) 236 s_offset = np.hstack((nweights, s_offset)) 237 s_details = make_details(s_info, s_length, s_offset, nweights+1) 238 v, w = weights[:nweights], weights[nweights:] 304 if radius_type > 0: 305 # If R_eff comes from form factor, make sure it is monodisperse. 306 # weight is set to 1 later, after the value array is created 307 s_length[0] = 1 308 s_details = make_details(s_info, s_length, s_offset, nweights) 239 309 s_values = [ 240 # scale=1, background=0, radius_effective=p_er, volfraction=s_vr 241 [1., 0., p_er, s_vr], 242 # structure factor parameters start after scale, background and 243 # all the form factor parameters. Skip the volfraction parameter 244 # as well, since it is computed elsewhere, and go to the end of the 245 # parameter list. 246 values[2+p_npars+1:2+p_npars+s_npars], 247 # no magnetism parameters to include for S 248 # add er into the (value, weights) pairs 249 v, [p_er], w, [1.0] 310 [1., 0.], # scale=1, background=0, 311 values[2+p_npars:2+p_npars+s_npars], 312 weights, 250 313 ] 251 314 spacer = (32 - sum(len(v) for v in s_values)%32)%32 … … 253 316 s_values = np.hstack(s_values).astype(self.s_kernel.dtype) 254 317 255 # Call the kernels 256 p_result = self.p_kernel(p_details, p_values, cutoff, magnetic) 257 s_result = self.s_kernel(s_details, s_values, cutoff, False) 258 259 #print("p_npars",p_npars,s_npars,p_er,s_vr,values[2+p_npars+1:2+p_npars+s_npars]) 260 #call_details.show(values) 261 #print("values", values) 262 #p_details.show(p_values) 263 #print("=>", p_result) 264 #s_details.show(s_values) 265 #print("=>", s_result) 266 267 # remember the parts for plotting later 268 self.results = [p_result, s_result] 269 270 #import pylab as plt 271 #plt.subplot(211); plt.loglog(self.p_kernel.q_input.q, p_result, '-') 272 #plt.subplot(212); plt.loglog(self.s_kernel.q_input.q, s_result, '-') 273 #plt.figure() 274 275 return values[0]*(p_result*s_result) + values[1] 318 # Call the form factor kernel to compute <F> and <F^2>. 319 # If the model doesn't support Fq the returned <F> will be None. 320 F1, F2, effective_radius, shell_volume, volume_ratio = self.p_kernel.Fq( 321 p_details, p_values, cutoff, magnetic, radius_type) 322 323 # Call the structure factor kernel to compute S. 324 # Plug R_eff from the form factor into structure factor parameters 325 # and scale volume fraction by form:shell volume ratio. These changes 326 # needs to be both in the initial value slot as well as the 327 # polydispersity distribution slot in the values array due to 328 # implementation details in kernel_iq.c. 329 #print("R_eff=%d:%g, volfrac=%g, volume ratio=%g"%(radius_type, effective_radius, volfrac, volume_ratio)) 330 if radius_type > 0: 331 # set the value to the model R_eff and set the weight to 1 332 s_values[2] = s_values[2+s_npars+s_offset[0]] = effective_radius 333 s_values[2+s_npars+s_offset[0]+nweights] = 1.0 334 s_values[3] = s_values[2+s_npars+s_offset[1]] = volfrac*volume_ratio 335 S = self.s_kernel.Iq(s_details, s_values, cutoff, False) 336 337 # Determine overall scale factor. Hollow shapes are weighted by 338 # shell_volume, so that is needed for volume normalization. For 339 # solid shapes we can use shell_volume as well since it is equal 340 # to form volume. 341 combined_scale = scale*volfrac/shell_volume 342 343 # Combine form factor and structure factor 344 #print("beta", beta_mode, F1, F2, S) 345 PS = F2 + F1**2*(S-1) if beta_mode else F2*S 346 final_result = combined_scale*PS + background 347 348 # Capture intermediate values so user can see them. These are 349 # returned as a lazy evaluator since they are only needed in the 350 # GUI, and not for each evaluation during a fit. 351 # TODO: return the results structure with the final results 352 # That way the model calcs are idempotent. Further, we can 353 # generalize intermediates to various other model types if we put it 354 # kernel calling interface. Could do this as an "optional" 355 # return value in the caller, though in that case we could return 356 # the results directly rather than through a lazy evaluator. 357 self.results = lambda: _intermediates( 358 F1, F2, S, combined_scale, effective_radius, beta_mode) 359 360 return final_result 276 361 277 362 def release(self): … … 279 364 self.p_kernel.release() 280 365 self.s_kernel.release() 281 282 283 def calc_er_vr(model_info, call_details, values):284 # type: (ModelInfo, ParameterSet) -> Tuple[float, float]285 286 if model_info.ER is None and model_info.VR is None:287 return 1.0, 1.0288 289 nvalues = model_info.parameters.nvalues290 value = values[nvalues:nvalues + call_details.num_weights]291 weight = values[nvalues + call_details.num_weights: nvalues + 2*call_details.num_weights]292 npars = model_info.parameters.npars293 # Note: changed from pairs ([v], [w]) to triples (p, [v], [w]), but the294 # dispersion mesh code doesn't actually care about the nominal parameter295 # value, p, so set it to None.296 pairs = [(None, value[offset:offset+length], weight[offset:offset+length])297 for p, offset, length298 in zip(model_info.parameters.call_parameters[2:2+npars],299 call_details.offset,300 call_details.length)301 if p.type == 'volume']302 value, weight = dispersion_mesh(model_info, pairs)303 304 if model_info.ER is not None:305 individual_radii = model_info.ER(*value)306 radius_effective = np.sum(weight*individual_radii) / np.sum(weight)307 else:308 radius_effective = 1.0309 310 if model_info.VR is not None:311 whole, part = model_info.VR(*value)312 volume_ratio = np.sum(weight*part)/np.sum(weight*whole)313 else:314 volume_ratio = 1.0315 316 return radius_effective, volume_ratio
Note: See TracChangeset
for help on using the changeset viewer.