Changeset fd7291e in sasmodels for sasmodels/product.py


Ignore:
Timestamp:
Mar 6, 2019 4:05:52 PM (5 years ago)
Author:
Paul Kienzle <pkienzle@…>
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.
Message:

Merge branch 'beta_approx' into ticket-1022-sum_multiplicity

File:
1 edited

Legend:

Unmodified
Added
Removed
  • sasmodels/product.py

    rb171acd rfd7291e  
    1313from __future__ import print_function, division 
    1414 
     15from collections import OrderedDict 
     16 
    1517from copy import copy 
    1618import numpy as np  # type: ignore 
    1719 
    18 from .modelinfo import ParameterTable, ModelInfo 
     20from .modelinfo import ParameterTable, ModelInfo, Parameter, parse_parameter 
    1921from .kernel import KernelModel, Kernel 
    2022from .details import make_details, dispersion_mesh 
     
    2224# pylint: disable=unused-import 
    2325try: 
    24     from typing import Tuple 
     26    from typing import Tuple, Callable, Union 
    2527except ImportError: 
    2628    pass 
     
    3537#] 
    3638 
    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. 
     39STRUCTURE_MODE_ID = "structure_factor_mode" 
     40RADIUS_MODE_ID = "radius_effective_mode" 
     41RADIUS_ID = "radius_effective" 
     42VOLFRAC_ID = "volfraction" 
     43def 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 
    4265def make_product_info(p_info, s_info): 
    4366    # type: (ModelInfo, ModelInfo) -> ModelInfo 
     
    5073    # have any magnetic parameters 
    5174    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 == VF_ID: 
    56         raise TypeError("S needs {} as second parameter".format(VF_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)) 
    5780    if not s_info.parameters.magnetism_index == []: 
    5881        raise TypeError("S should not have SLD parameters") 
     
    6083    s_id, s_name, s_pars = s_info.id, s_info.name, s_info.parameters 
    6184 
    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 
    6486    # are any names in P that overlap with those in S, modify the name in S 
    6587    # to distinguish it. 
    6688    p_set = set(p.id for p in p_pars.kernel_parameters) 
    6789    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] 
    6991    # Check if still a collision after renaming.  This could happen if for 
    7092    # example S has volfrac and P has both volfrac and volfrac_S. 
     
    7294        raise TypeError("name collision: P has P.name and P.name_S while S has S.name") 
    7395 
     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 
    74100    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) 
    77103    parameters = ParameterTable(combined_pars) 
    78104    parameters.max_pd = p_pars.max_pd + s_pars.max_pd 
    79105    def random(): 
    80106        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) 
    82108        combined_pars.update((translate_name[k], v) 
    83109                             for k, v in s_info.random().items() 
     
    100126    #model_info.tests = [] 
    101127    #model_info.source = [] 
    102     # Iq, Iqxy, form_volume, ER, VR and sesans 
    103128    # Remember the component info blocks so we can build the model 
    104129    model_info.composition = ('product', [p_info, s_info]) 
     
    141166    return par 
    142167 
     168def _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 
    143200class ProductModel(KernelModel): 
    144201    def __init__(self, model_info, P, S): 
     
    150207        #: Structure factor modelling interaction between particles. 
    151208        self.S = S 
     209 
    152210        #: Model precision. This is not really relevant, since it is the 
    153211        #: individual P and S models that control the effective dtype, 
     
    167225        # in opencl; or both in opencl, but one in single precision and the 
    168226        # other in double precision). 
     227 
    169228        p_kernel = self.P.make_kernel(q_vectors) 
    170229        s_kernel = self.S.make_kernel(q_vectors) 
     
    191250    def __call__(self, call_details, values, cutoff, magnetic): 
    192251        # type: (CallDetails, np.ndarray, float, bool) -> np.ndarray 
     252 
    193253        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] 
    194279 
    195280        # if there are magnetic parameters, they will only be on the 
     
    206291 
    207292        # Construct the calling parameters for P. 
    208         p_npars = p_info.parameters.npars 
    209         p_length = call_details.length[:p_npars] 
    210         p_offset = call_details.offset[:p_npars] 
    211293        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] 
    216299        spacer = (32 - sum(len(v) for v in p_values)%32)%32 
    217300        p_values.append([0.]*spacer) 
    218301        p_values = np.hstack(p_values).astype(self.p_kernel.dtype) 
    219302 
    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  
    225303        # 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) 
    239309        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, 
    250313        ] 
    251314        spacer = (32 - sum(len(v) for v in s_values)%32)%32 
     
    253316        s_values = np.hstack(s_values).astype(self.s_kernel.dtype) 
    254317 
    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 
    276361 
    277362    def release(self): 
     
    279364        self.p_kernel.release() 
    280365        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.0 
    288  
    289     nvalues = model_info.parameters.nvalues 
    290     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.npars 
    293     # Note: changed from pairs ([v], [w]) to triples (p, [v], [w]), but the 
    294     # dispersion mesh code doesn't actually care about the nominal parameter 
    295     # value, p, so set it to None. 
    296     pairs = [(None, value[offset:offset+length], weight[offset:offset+length]) 
    297              for p, offset, length 
    298              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.0 
    309  
    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.0 
    315  
    316     return radius_effective, volume_ratio 
Note: See TracChangeset for help on using the changeset viewer.