Changeset 69aa451 in sasmodels for sasmodels/generate.py


Ignore:
Timestamp:
Mar 23, 2016 11:39:08 AM (8 years ago)
Author:
Paul Kienzle <pkienzle@…>
Branches:
master, core_shell_microgels, costrafo411, magnetic_model, release_v0.94, release_v0.95, ticket-1257-vesicle-product, ticket_1156, ticket_1265_superball, ticket_822_more_unit_tests
Children:
e9b1663d
Parents:
6fd8de0
Message:

refactor parameter representation

File:
1 edited

Legend:

Unmodified
Added
Removed
  • sasmodels/generate.py

    rea1f14d r69aa451  
    6969The constructor code will generate the necessary vectors for computing 
    7070them with the desired polydispersity. 
    71  
    72 The available kernel parameters are defined as a list, with each parameter 
    73 defined as a sublist with the following elements: 
    74  
    75     *name* is the name that will be used in the call to the kernel 
    76     function and the name that will be displayed to the user.  Names 
    77     should be lower case, with words separated by underscore.  If 
    78     acronyms are used, the whole acronym should be upper case. 
    79  
    80     *units* should be one of *degrees* for angles, *Ang* for lengths, 
    81     *1e-6/Ang^2* for SLDs. 
    82  
    83     *default value* will be the initial value for  the model when it 
    84     is selected, or when an initial value is not otherwise specified. 
    85  
    86     *limits = [lb, ub]* are the hard limits on the parameter value, used to 
    87     limit the polydispersity density function.  In the fit, the parameter limits 
    88     given to the fit are the limits  on the central value of the parameter. 
    89     If there is polydispersity, it will evaluate parameter values outside 
    90     the fit limits, but not outside the hard limits specified in the model. 
    91     If there are no limits, use +/-inf imported from numpy. 
    92  
    93     *type* indicates how the parameter will be used.  "volume" parameters 
    94     will be used in all functions.  "orientation" parameters will be used 
    95     in *Iqxy* and *Imagnetic*.  "magnetic* parameters will be used in 
    96     *Imagnetic* only.  If *type* is the empty string, the parameter will 
    97     be used in all of *Iq*, *Iqxy* and *Imagnetic*.  "sld" parameters 
    98     can automatically be promoted to magnetic parameters, each of which 
    99     will have a magnitude and a direction, which may be different from 
    100     other sld parameters. 
    101  
    102     *description* is a short description of the parameter.  This will 
    103     be displayed in the parameter table and used as a tool tip for the 
    104     parameter value in the user interface. 
    105  
    10671The kernel module must set variables defining the kernel meta data: 
    10772 
     
    216181from __future__ import print_function 
    217182 
    218 # TODO: identify model files which have changed since loading and reload them. 
     183#TODO: determine which functions are useful outside of generate 
     184#__all__ = ["model_info", "make_doc", "make_source", "convert_type"] 
    219185 
    220186import sys 
     
    224190import string 
    225191import warnings 
    226 from collections import namedtuple 
    227192 
    228193import numpy as np 
    229194 
    230 # TODO: promote Parameter and model_info to classes 
    231 PARAMETER_FIELDS = ['name', 'units', 'default', 'limits', 'type', 'description'] 
    232 Parameter = namedtuple('Parameter', PARAMETER_FIELDS) 
    233  
    234 #TODO: determine which functions are useful outside of generate 
    235 #__all__ = ["model_info", "make_doc", "make_source", "convert_type"] 
     195from .modelinfo import ModelInfo, Parameter, make_parameter_table 
     196 
     197# TODO: identify model files which have changed since loading and reload them. 
    236198 
    237199TEMPLATE_ROOT = dirname(__file__) 
     
    246208except TypeError: 
    247209    F128 = None 
    248  
    249 # Scale and background, which are parameters common to every form factor 
    250 COMMON_PARAMETERS = [ 
    251     ["scale", "", 1, [0, np.inf], "", "Source intensity"], 
    252     ["background", "1/cm", 1e-3, [0, np.inf], "", "Source background"], 
    253     ] 
    254210 
    255211# Conversion from units defined in the parameter table for each model 
     
    354310    return [_search(search_path, f) for f in model_info['source']] 
    355311 
     312def timestamp(model_info): 
     313    """ 
     314    Return a timestamp for the model corresponding to the most recently 
     315    changed file or dependency. 
     316    """ 
     317    source_files = (model_sources(model_info) 
     318                    + model_templates() 
     319                    + [model_info['filename']]) 
     320    newest = max(getmtime(f) for f in source_files) 
     321    return newest 
     322 
    356323def convert_type(source, dtype): 
    357324    """ 
     
    448415         } 
    449416    """ 
    450     par_decl = ', '.join('double ' + p for p in pars) if pars else 'void' 
     417    par_decl = ', '.join(p.as_argument() for p in pars) if pars else 'void' 
    451418    return _FN_TEMPLATE % {'name': name, 'body': body, 'pars': par_decl} 
    452419 
     
    455422    Return a list of *prefix.parameter* from parameter items. 
    456423    """ 
    457     prefix += "." 
    458     return [prefix+p for p in pars] 
     424    return [p.as_call_reference(prefix) for p in pars] 
    459425 
    460426_IQXY_PATTERN = re.compile("^((inline|static) )? *(double )? *Iqxy *([(]|$)", 
     
    499465    # for computing volume even if we allow non-disperse volume parameters. 
    500466 
    501     # kernel_iq assumes scale and background are the first parameters; 
    502     # they should be first for 1d and 2d parameter lists as well. 
    503     assert model_info['parameters'][0].name == 'scale' 
    504     assert model_info['parameters'][1].name == 'background' 
    505  
    506     # Identify parameter types 
    507     iq_parameters = model_info['par_type']['1d'][2:] 
    508     iqxy_parameters = model_info['par_type']['2d'][2:] 
    509     vol_parameters = model_info['par_type']['volume'] 
     467    partable = model_info['parameters'] 
     468 
     469    # Identify parameters for Iq, Iqxy, Iq_magnetic and form_volume. 
     470    # Note that scale and volume are not possible types. 
    510471 
    511472    # Load templates and user code 
     
    517478    source = [kernel_header] + user_code 
    518479 
     480    vol_parameters = partable.kernel_pars('volume') 
     481    iq_parameters = partable.kernel_pars('1d') 
     482    iqxy_parameters = partable.kernel_pars('2d') 
     483 
     484    # Make parameters for q, qx, qy so that we can use them in declarations 
     485    q, qx, qy = [Parameter(name=v) for v in ('q', 'qx', 'qy')] 
    519486    # Generate form_volume function, etc. from body only 
    520487    if model_info['form_volume'] is not None: 
    521         pnames = [p for p in vol_parameters] 
    522         source.append(_gen_fn('form_volume', pnames, model_info['form_volume'])) 
     488        pars = vol_parameters 
     489        source.append(_gen_fn('form_volume', pars, model_info['form_volume'])) 
    523490    if model_info['Iq'] is not None: 
    524         pnames = ['q'] + [p for p in iq_parameters] 
    525         source.append(_gen_fn('Iq', pnames, model_info['Iq'])) 
     491        pars = [q] + iq_parameters 
     492        source.append(_gen_fn('Iq', pars, model_info['Iq'])) 
    526493    if model_info['Iqxy'] is not None: 
    527         pnames = ['qx', 'qy'] + [p for p in iqxy_parameters] 
    528         source.append(_gen_fn('Iqxy', pnames, model_info['Iqxy'])) 
     494        pars = [qx, qy] + iqxy_parameters 
     495        source.append(_gen_fn('Iqxy', pars, model_info['Iqxy'])) 
    529496 
    530497    # Define the parameter table 
    531498    source.append("#define PARAMETER_TABLE \\") 
    532     source.append("\\\n    ".join("double %s;"%p.name 
    533                                    for p in model_info['parameters'][2:])) 
     499    source.append("\\\n".join(p.as_definition() 
     500                                  for p in model_info['parameters'][2:])) 
    534501 
    535502    # Define the function calls 
    536503    if vol_parameters: 
    537         refs = ",".join(_call_pars("v", vol_parameters)) 
    538         call_volume = "#define CALL_VOLUME(v) form_volume(%s)"%refs 
     504        refs = _call_pars("v.", vol_parameters) 
     505        call_volume = "#define CALL_VOLUME(v) form_volume(%s)" % (",".join(refs)) 
    539506    else: 
    540507        # Model doesn't have volume.  We could make the kernel run a little 
     
    544511    source.append(call_volume) 
    545512 
    546     refs = ["q[i]"] + _call_pars("v", iq_parameters) 
     513    refs = ["q[i]"] + _call_pars("v.", iq_parameters) 
    547514    call_iq = "#define CALL_IQ(q,i,v) Iq(%s)" % (",".join(refs)) 
    548515    if _have_Iqxy(user_code): 
    549516        # Call 2D model 
    550         refs = ["q[2*i]", "q[2*i+1]"] + _call_pars("v", iqxy_parameters) 
     517        refs = ["q[2*i]", "q[2*i+1]"] + _call_pars("v.", iqxy_parameters) 
    551518        call_iqxy = "#define CALL_IQ(q,i,v) Iqxy(%s)" % (",".join(refs)) 
    552519    else: 
     
    559526    # Fill in definitions for numbers of parameters 
    560527    source.append("#define MAX_PD %s"%model_info['max_pd']) 
    561     source.append("#define NPARS %d"%(len(model_info['parameters'])-2)) 
     528    source.append("#define NPARS %d"%(len(partable.kernel_pars()))) 
    562529 
    563530    # TODO: allow mixed python/opencl kernels? 
     
    597564    """ 
    598565    par_set = {} 
    599     par_set['1d'] = [p.name for p in pars if p.type not in ('orientation', 'magnetic')] 
    600     par_set['2d'] = [p.name for p in pars if p.type != 'magnetic'] 
    601     par_set['magnetic'] = [p.name for p in pars] 
    602     par_set['pd'] = [p.name for p in pars if p.type in ('volume', 'orientation')] 
    603     par_set['pd_relative'] = [p.name for p in pars if p.type == 'volume'] 
    604     if 'theta' in par_set['2d']: 
    605         # find id of theta in parameter set (or whatever variable is 
    606         # used for spherical normalization during polydispersity... 
    607         par_set['theta_par'] = [k for k,p in enumerate(pars) if p.name=='theta'][0] 
    608     else: 
    609         par_set['theta_par'] = -1 
    610     return par_set 
    611  
    612 def collect_types(pars): 
    613     """ 
    614     Build parameter categories out of the the parameter definitions. 
    615  
    616     Returns a dictionary of categories. 
    617  
    618     Note: these categories are subject to change, depending on the needs of 
    619     the UI and the needs of the kernel calling function. 
    620  
    621     The categories are as follows: 
    622  
    623     * *volume* list of volume parameter names 
    624     * *orientation* list of orientation parameters 
    625     * *magnetic* list of magnetic parameters 
    626     * *sld* list of parameters that have no type info 
    627     * *other* list of parameters that have no type info 
    628  
    629     Each parameter is in one and only one category. 
    630     """ 
    631     par_type = { 
    632         'volume': [], 'orientation': [], 'magnetic': [], 'sld': [], 'other': [], 
    633     } 
    634     for p in pars: 
    635         par_type[p.type if p.type else 'other'].append(p.name) 
    636     return  par_type 
    637  
    638566 
    639567def process_parameters(model_info): 
     
    641569    Process parameter block, precalculating parameter details. 
    642570    """ 
    643     # convert parameters into named tuples 
    644     for p in model_info['parameters']: 
    645         if p[4] == '' and (p[0].startswith('sld') or p[0].endswith('sld')): 
    646             p[4] = 'sld' 
    647             # TODO: make sure all models explicitly label their sld parameters 
    648             #raise ValueError("%s.%s needs to be explicitly set to type 'sld'" %(model_info['id'], p[0])) 
    649  
    650     pars = [Parameter(*p) for p in model_info['parameters']] 
    651     # Fill in the derived attributes 
    652     par_type = collect_types(pars) 
    653     par_type.update(categorize_parameters(pars)) 
    654     model_info['parameters'] = pars 
    655     model_info['par_type'] = par_type 
     571    partable = model_info['parameters'] 
    656572    if model_info.get('demo', None) is None: 
    657         model_info['demo'] = dict((p.name, p.default) for p in pars) 
    658     model_info['has_2d'] = par_type['orientation'] or par_type['magnetic'] 
     573        model_info['demo'] = partable.defaults 
     574 
    659575    # Don't use more polydisperse parameters than are available in the model 
    660576    # Note: we can do polydispersity on arbitrary parameters, so it is not 
    661577    # clear that this is a good idea; it does however make the poly_details 
    662578    # code easier to write, so we will leave it in for now. 
    663     model_info['max_pd'] = min(len(par_type['pd']), MAX_PD) 
     579    model_info['max_pd'] = min(partable.num_pd, MAX_PD) 
    664580 
    665581def mono_details(model_info): 
     582    # TODO: move max_pd into ParameterTable? 
    666583    max_pd = model_info['max_pd'] 
    667     npars = len(model_info['parameters']) - 2 
    668     p = 5*max_pd 
    669     c = p + 3*npars 
    670  
    671     details = np.zeros(c + 2, 'int32') 
     584    pars = model_info['parameters'].kernel_pars() 
     585    npars = len(pars) 
     586    par_offset = 5*max_pd 
     587    constants_offset = par_offset + 3*npars 
     588 
     589    details = np.zeros(constants_offset + 2, 'int32') 
    672590    details[0*max_pd:1*max_pd] = range(max_pd)       # pd_par: arbitrary order; use first 
    673591    details[1*max_pd:2*max_pd] = [1]*max_pd          # pd_length: only one element 
     
    675593    details[3*max_pd:4*max_pd] = [1]*max_pd          # pd_stride: vectors of length 1 
    676594    details[4*max_pd:5*max_pd] = [0]*max_pd          # pd_isvol: doens't matter if no norm 
    677     details[p+0*npars:p+1*npars] = range(2, npars+2) # par_offset: skip scale and background 
    678     details[p+1*npars:p+2*npars] = [0]*npars         # no coordination 
     595    details[par_offset+0*npars:par_offset+1*npars] = range(2, npars+2) # par_offset: skip scale and background 
     596    details[par_offset+1*npars:par_offset+2*npars] = [0]*npars         # no coordination 
    679597    #details[p+npars] = 1 # par_coord[0] is coordinated with the first par? 
    680     details[p+2*npars:p+3*npars] = 0 # fast coord with 0 
    681     details[c]   = 1     # fast_coord_count: one fast index 
    682     details[c+1] = -1    # theta_par: None 
     598    details[par_offset+2*npars:par_offset+3*npars] = 0 # fast coord with 0 
     599    details[constants_offset]   = 1     # fast_coord_count: one fast index 
     600    details[constants_offset+1] = -1    # theta_par: None 
    683601    return details 
    684602 
    685603def poly_details(model_info, weights): 
    686     print("entering poly",weights) 
    687  
    688     print([p.name for p in model_info['parameters']]) 
    689     pars = model_info['parameters'][2:]  # skip scale and background 
    690604    weights = weights[2:] 
     605 
     606    # TODO: move max_pd into ParameterTable? 
    691607    max_pd = model_info['max_pd'] 
    692     npars = len(pars) # scale and background already removed 
     608    pars = model_info['parameters'].kernel_pars 
     609    npars = len(pars) 
    693610    par_offset = 5*max_pd 
    694611    constants_offset = par_offset + 3*npars 
     
    745662    """ 
    746663    if model_info['Iq'] is not None and model_info['Iqxy'] is None: 
    747         if model_info['par_type']['1d'] != model_info['par_type']['2d']: 
     664        partable = model_info['parameters'] 
     665        if partable.type['1d'] != partable.type['2d']: 
    748666            raise ValueError("Iqxy model is missing") 
    749667        Iq = model_info['Iq'] 
     
    751669            return Iq(np.sqrt(qx**2 + qy**2), **kw) 
    752670        model_info['Iqxy'] = Iqxy 
     671 
    753672 
    754673def make_model_info(kernel_module): 
     
    800719    """ 
    801720    # TODO: maybe turn model_info into a class ModelDefinition 
    802     parameters = COMMON_PARAMETERS + kernel_module.parameters 
     721    #print("make parameter table", kernel_module.parameters) 
     722    parameters = make_parameter_table(kernel_module.parameters) 
    803723    filename = abspath(kernel_module.__file__) 
    804724    kernel_id = splitext(basename(filename))[0] 
Note: See TracChangeset for help on using the changeset viewer.