Changeset 3543141 in sasmodels for sasmodels/generate.py


Ignore:
Timestamp:
Apr 7, 2016 2:06:03 PM (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:
a8a7f08
Parents:
6e7ff6d (diff), 65279d8 (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 remote-tracking branch 'origin/master' into polydisp

File:
1 edited

Legend:

Unmodified
Added
Removed
  • sasmodels/generate.py

    re6408d0 r6e7ff6d  
    2121 
    2222    *VR(p1, p2, ...)* returns the volume ratio for core-shell style forms. 
     23 
     24    #define INVALID(v) (expr)  returns False if v.parameter is invalid 
     25    for some parameter or other (e.g., v.bell_radius < v.radius).  If 
     26    necessary, the expression can call a function. 
    2327 
    2428These functions are defined in a kernel module .py script and an associated 
     
    6569The constructor code will generate the necessary vectors for computing 
    6670them with the desired polydispersity. 
    67  
    68 The available kernel parameters are defined as a list, with each parameter 
    69 defined as a sublist with the following elements: 
    70  
    71     *name* is the name that will be used in the call to the kernel 
    72     function and the name that will be displayed to the user.  Names 
    73     should be lower case, with words separated by underscore.  If 
    74     acronyms are used, the whole acronym should be upper case. 
    75  
    76     *units* should be one of *degrees* for angles, *Ang* for lengths, 
    77     *1e-6/Ang^2* for SLDs. 
    78  
    79     *default value* will be the initial value for  the model when it 
    80     is selected, or when an initial value is not otherwise specified. 
    81  
    82     *limits = [lb, ub]* are the hard limits on the parameter value, used to 
    83     limit the polydispersity density function.  In the fit, the parameter limits 
    84     given to the fit are the limits  on the central value of the parameter. 
    85     If there is polydispersity, it will evaluate parameter values outside 
    86     the fit limits, but not outside the hard limits specified in the model. 
    87     If there are no limits, use +/-inf imported from numpy. 
    88  
    89     *type* indicates how the parameter will be used.  "volume" parameters 
    90     will be used in all functions.  "orientation" parameters will be used 
    91     in *Iqxy* and *Imagnetic*.  "magnetic* parameters will be used in 
    92     *Imagnetic* only.  If *type* is the empty string, the parameter will 
    93     be used in all of *Iq*, *Iqxy* and *Imagnetic*.  "sld" parameters 
    94     can automatically be promoted to magnetic parameters, each of which 
    95     will have a magnitude and a direction, which may be different from 
    96     other sld parameters. 
    97  
    98     *description* is a short description of the parameter.  This will 
    99     be displayed in the parameter table and used as a tool tip for the 
    100     parameter value in the user interface. 
    101  
    10271The kernel module must set variables defining the kernel meta data: 
    10372 
     
    156125An *model_info* dictionary is constructed from the kernel meta data and 
    157126returned to the caller. 
    158  
    159 The model evaluator, function call sequence consists of q inputs and the return vector, 
    160 followed by the loop value/weight vector, followed by the values for 
    161 the non-polydisperse parameters, followed by the lengths of the 
    162 polydispersity loops.  To construct the call for 1D models, the 
    163 categories *fixed-1d* and *pd-1d* list the names of the parameters 
    164 of the non-polydisperse and the polydisperse parameters respectively. 
    165 Similarly, *fixed-2d* and *pd-2d* provide parameter names for 2D models. 
    166 The *pd-rel* category is a set of those parameters which give 
    167 polydispersitiy as a portion of the value (so a 10% length dispersity 
    168 would use a polydispersity value of 0.1) rather than absolute 
    169 dispersity such as an angle plus or minus 15 degrees. 
    170  
    171 The *volume* category lists the volume parameters in order for calls 
    172 to volume within the kernel (used for volume normalization) and for 
    173 calls to ER and VR for effective radius and volume ratio respectively. 
    174  
    175 The *orientation* and *magnetic* categories list the orientation and 
    176 magnetic parameters.  These are used by the sasview interface.  The 
    177 blank category is for parameters such as scale which don't have any 
    178 other marking. 
    179127 
    180128The doc string at the start of the kernel module will be used to 
     
    204152from __future__ import print_function 
    205153 
    206 #TODO: identify model files which have changed since loading and reload them. 
    207154#TODO: determine which functions are useful outside of generate 
    208155#__all__ = ["model_info", "make_doc", "make_source", "convert_type"] 
    209156 
    210 import sys 
    211157from os.path import abspath, dirname, join as joinpath, exists, basename, \ 
    212     splitext 
     158    splitext, getmtime 
    213159import re 
    214160import string 
    215161import warnings 
    216 from collections import namedtuple 
    217162 
    218163import numpy as np 
    219164 
     165from .modelinfo import ModelInfo, Parameter, make_parameter_table, set_demo 
    220166from .custom import load_custom_kernel_module 
    221167 
    222 PARAMETER_FIELDS = ['name', 'units', 'default', 'limits', 'type', 'description'] 
    223 Parameter = namedtuple('Parameter', PARAMETER_FIELDS) 
    224  
    225 C_KERNEL_TEMPLATE_PATH = joinpath(dirname(__file__), 'kernel_template.c') 
     168TEMPLATE_ROOT = dirname(__file__) 
    226169 
    227170F16 = np.dtype('float16') 
     
    232175except TypeError: 
    233176    F128 = None 
    234  
    235 # Scale and background, which are parameters common to every form factor 
    236 COMMON_PARAMETERS = [ 
    237     ["scale", "", 1, [0, np.inf], "", "Source intensity"], 
    238     ["background", "1/cm", 1e-3, [0, np.inf], "", "Source background"], 
    239     ] 
    240177 
    241178# Conversion from units defined in the parameter table for each model 
     
    331268    raise ValueError("%r not found in %s" % (filename, search_path)) 
    332269 
     270 
    333271def model_sources(model_info): 
    334272    """ 
     
    339277    return [_search(search_path, f) for f in model_info['source']] 
    340278 
    341 # Pragmas for enable OpenCL features.  Be sure to protect them so that they 
    342 # still compile even if OpenCL is not present. 
    343 _F16_PRAGMA = """\ 
    344 #if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp16) 
    345 #  pragma OPENCL EXTENSION cl_khr_fp16: enable 
    346 #endif 
    347 """ 
    348  
    349 _F64_PRAGMA = """\ 
    350 #if defined(__OPENCL_VERSION__) // && !defined(cl_khr_fp64) 
    351 #  pragma OPENCL EXTENSION cl_khr_fp64: enable 
    352 #endif 
    353 """ 
     279def timestamp(model_info): 
     280    """ 
     281    Return a timestamp for the model corresponding to the most recently 
     282    changed file or dependency. 
     283    """ 
     284    source_files = (model_sources(model_info) 
     285                    + model_templates() 
     286                    + [model_info['filename']]) 
     287    newest = max(getmtime(f) for f in source_files) 
     288    return newest 
    354289 
    355290def convert_type(source, dtype): 
     
    362297    if dtype == F16: 
    363298        fbytes = 2 
    364         source = _F16_PRAGMA + _convert_type(source, "half", "f") 
     299        source = _convert_type(source, "half", "f") 
    365300    elif dtype == F32: 
    366301        fbytes = 4 
     
    368303    elif dtype == F64: 
    369304        fbytes = 8 
    370         source = _F64_PRAGMA + source  # Source is already double 
     305        # no need to convert if it is already double 
    371306    elif dtype == F128: 
    372307        fbytes = 16 
     
    411346 
    412347 
    413 LOOP_OPEN = """\ 
    414 for (int %(name)s_i=0; %(name)s_i < N%(name)s; %(name)s_i++) { 
    415   const double %(name)s = loops[2*(%(name)s_i%(offset)s)]; 
    416   const double %(name)s_w = loops[2*(%(name)s_i%(offset)s)+1];\ 
     348_template_cache = {} 
     349def load_template(filename): 
     350    path = joinpath(TEMPLATE_ROOT, filename) 
     351    mtime = getmtime(path) 
     352    if filename not in _template_cache or mtime > _template_cache[filename][0]: 
     353        with open(path) as fid: 
     354            _template_cache[filename] = (mtime, fid.read(), path) 
     355    return _template_cache[filename][1] 
     356 
     357def model_templates(): 
     358    # TODO: fails DRY; templates are listed in two places. 
     359    # should instead have model_info contain a list of paths 
     360    return [joinpath(TEMPLATE_ROOT, filename) 
     361            for filename in ('kernel_header.c', 'kernel_iq.c')] 
     362 
     363 
     364_FN_TEMPLATE = """\ 
     365double %(name)s(%(pars)s); 
     366double %(name)s(%(pars)s) { 
     367    %(body)s 
     368} 
     369 
     370 
    417371""" 
    418 def build_polydispersity_loops(pd_pars): 
    419     """ 
    420     Build polydispersity loops 
    421  
    422     Returns loop opening and loop closing 
    423     """ 
    424     depth = 4 
    425     offset = "" 
    426     loop_head = [] 
    427     loop_end = [] 
    428     for name in pd_pars: 
    429         subst = {'name': name, 'offset': offset} 
    430         loop_head.append(indent(LOOP_OPEN % subst, depth)) 
    431         loop_end.insert(0, (" "*depth) + "}") 
    432         offset += '+N' + name 
    433         depth += 2 
    434     return "\n".join(loop_head), "\n".join(loop_end) 
    435  
    436 C_KERNEL_TEMPLATE = None 
     372 
     373def _gen_fn(name, pars, body): 
     374    """ 
     375    Generate a function given pars and body. 
     376 
     377    Returns the following string:: 
     378 
     379         double fn(double a, double b, ...); 
     380         double fn(double a, double b, ...) { 
     381             .... 
     382         } 
     383    """ 
     384    par_decl = ', '.join(p.as_function_argument() for p in pars) if pars else 'void' 
     385    return _FN_TEMPLATE % {'name': name, 'body': body, 'pars': par_decl} 
     386 
     387def _call_pars(prefix, pars): 
     388    """ 
     389    Return a list of *prefix.parameter* from parameter items. 
     390    """ 
     391    return [p.as_call_reference(prefix) for p in pars] 
     392 
     393_IQXY_PATTERN = re.compile("^((inline|static) )? *(double )? *Iqxy *([(]|$)", 
     394                           flags=re.MULTILINE) 
     395def _have_Iqxy(sources): 
     396    """ 
     397    Return true if any file defines Iqxy. 
     398 
     399    Note this is not a C parser, and so can be easily confused by 
     400    non-standard syntax.  Also, it will incorrectly identify the following 
     401    as having Iqxy:: 
     402 
     403        /* 
     404        double Iqxy(qx, qy, ...) { ... fill this in later ... } 
     405        */ 
     406 
     407    If you want to comment out an Iqxy function, use // on the front of the 
     408    line instead. 
     409    """ 
     410    for code in sources: 
     411        if _IQXY_PATTERN.search(code): 
     412            return True 
     413    else: 
     414        return False 
     415 
    437416def make_source(model_info): 
    438417    """ 
     
    453432    # for computing volume even if we allow non-disperse volume parameters. 
    454433 
    455     # Load template 
    456     global C_KERNEL_TEMPLATE 
    457     if C_KERNEL_TEMPLATE is None: 
    458         with open(C_KERNEL_TEMPLATE_PATH) as fid: 
    459             C_KERNEL_TEMPLATE = fid.read() 
    460  
    461     # Load additional sources 
    462     source = [open(f).read() for f in model_sources(model_info)] 
    463  
    464     # Prepare defines 
    465     defines = [] 
    466     partype = model_info['partype'] 
    467     pd_1d = partype['pd-1d'] 
    468     pd_2d = partype['pd-2d'] 
    469     fixed_1d = partype['fixed-1d'] 
    470     fixed_2d = partype['fixed-1d'] 
    471  
    472     iq_parameters = [p.name 
    473                      for p in model_info['parameters'][2:]  # skip scale, background 
    474                      if p.name in set(fixed_1d + pd_1d)] 
    475     iqxy_parameters = [p.name 
    476                        for p in model_info['parameters'][2:]  # skip scale, background 
    477                        if p.name in set(fixed_2d + pd_2d)] 
    478     volume_parameters = [p.name 
    479                          for p in model_info['parameters'] 
    480                          if p.type == 'volume'] 
    481  
    482     # Fill in defintions for volume parameters 
    483     if volume_parameters: 
    484         defines.append(('VOLUME_PARAMETERS', 
    485                         ','.join(volume_parameters))) 
    486         defines.append(('VOLUME_WEIGHT_PRODUCT', 
    487                         '*'.join(p + '_w' for p in volume_parameters))) 
    488  
    489     # Generate form_volume function from body only 
     434    partable = model_info['parameters'] 
     435 
     436    # Identify parameters for Iq, Iqxy, Iq_magnetic and form_volume. 
     437    # Note that scale and volume are not possible types. 
     438 
     439    # Load templates and user code 
     440    kernel_header = load_template('kernel_header.c') 
     441    kernel_code = load_template('kernel_iq.c') 
     442    user_code = [open(f).read() for f in model_sources(model_info)] 
     443 
     444    # Build initial sources 
     445    source = [kernel_header] + user_code 
     446 
     447    # Make parameters for q, qx, qy so that we can use them in declarations 
     448    q, qx, qy = [Parameter(name=v) for v in ('q', 'qx', 'qy')] 
     449    # Generate form_volume function, etc. from body only 
    490450    if model_info['form_volume'] is not None: 
    491         if volume_parameters: 
    492             vol_par_decl = ', '.join('double ' + p for p in volume_parameters) 
    493         else: 
    494             vol_par_decl = 'void' 
    495         defines.append(('VOLUME_PARAMETER_DECLARATIONS', 
    496                         vol_par_decl)) 
    497         fn = """\ 
    498 double form_volume(VOLUME_PARAMETER_DECLARATIONS); 
    499 double form_volume(VOLUME_PARAMETER_DECLARATIONS) { 
    500     %(body)s 
    501 } 
    502 """ % {'body':model_info['form_volume']} 
    503         source.append(fn) 
    504  
    505     # Fill in definitions for Iq parameters 
    506     defines.append(('IQ_KERNEL_NAME', model_info['name'] + '_Iq')) 
    507     defines.append(('IQ_PARAMETERS', ', '.join(iq_parameters))) 
    508     if fixed_1d: 
    509         defines.append(('IQ_FIXED_PARAMETER_DECLARATIONS', 
    510                         ', \\\n    '.join('const double %s' % p for p in fixed_1d))) 
    511     if pd_1d: 
    512         defines.append(('IQ_WEIGHT_PRODUCT', 
    513                         '*'.join(p + '_w' for p in pd_1d))) 
    514         defines.append(('IQ_DISPERSION_LENGTH_DECLARATIONS', 
    515                         ', \\\n    '.join('const int N%s' % p for p in pd_1d))) 
    516         defines.append(('IQ_DISPERSION_LENGTH_SUM', 
    517                         '+'.join('N' + p for p in pd_1d))) 
    518         open_loops, close_loops = build_polydispersity_loops(pd_1d) 
    519         defines.append(('IQ_OPEN_LOOPS', 
    520                         open_loops.replace('\n', ' \\\n'))) 
    521         defines.append(('IQ_CLOSE_LOOPS', 
    522                         close_loops.replace('\n', ' \\\n'))) 
     451        pars = partable.form_volume_parameters 
     452        source.append(_gen_fn('form_volume', pars, model_info['form_volume'])) 
    523453    if model_info['Iq'] is not None: 
    524         defines.append(('IQ_PARAMETER_DECLARATIONS', 
    525                         ', '.join('double ' + p for p in iq_parameters))) 
    526         fn = """\ 
    527 double Iq(double q, IQ_PARAMETER_DECLARATIONS); 
    528 double Iq(double q, IQ_PARAMETER_DECLARATIONS) { 
    529     %(body)s 
    530 } 
    531 """ % {'body':model_info['Iq']} 
    532         source.append(fn) 
    533  
    534     # Fill in definitions for Iqxy parameters 
    535     defines.append(('IQXY_KERNEL_NAME', model_info['name'] + '_Iqxy')) 
    536     defines.append(('IQXY_PARAMETERS', ', '.join(iqxy_parameters))) 
    537     if fixed_2d: 
    538         defines.append(('IQXY_FIXED_PARAMETER_DECLARATIONS', 
    539                         ', \\\n    '.join('const double %s' % p for p in fixed_2d))) 
    540     if pd_2d: 
    541         defines.append(('IQXY_WEIGHT_PRODUCT', 
    542                         '*'.join(p + '_w' for p in pd_2d))) 
    543         defines.append(('IQXY_DISPERSION_LENGTH_DECLARATIONS', 
    544                         ', \\\n    '.join('const int N%s' % p for p in pd_2d))) 
    545         defines.append(('IQXY_DISPERSION_LENGTH_SUM', 
    546                         '+'.join('N' + p for p in pd_2d))) 
    547         open_loops, close_loops = build_polydispersity_loops(pd_2d) 
    548         defines.append(('IQXY_OPEN_LOOPS', 
    549                         open_loops.replace('\n', ' \\\n'))) 
    550         defines.append(('IQXY_CLOSE_LOOPS', 
    551                         close_loops.replace('\n', ' \\\n'))) 
     454        pars = [q] + partable.iq_parameters 
     455        source.append(_gen_fn('Iq', pars, model_info['Iq'])) 
    552456    if model_info['Iqxy'] is not None: 
    553         defines.append(('IQXY_PARAMETER_DECLARATIONS', 
    554                         ', '.join('double ' + p for p in iqxy_parameters))) 
    555         fn = """\ 
    556 double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS); 
    557 double Iqxy(double qx, double qy, IQXY_PARAMETER_DECLARATIONS) { 
    558     %(body)s 
    559 } 
    560 """ % {'body':model_info['Iqxy']} 
    561         source.append(fn) 
    562  
    563     # Need to know if we have a theta parameter for Iqxy; it is not there 
    564     # for the magnetic sphere model, for example, which has a magnetic 
    565     # orientation but no shape orientation. 
    566     if 'theta' in pd_2d: 
    567         defines.append(('IQXY_HAS_THETA', '1')) 
    568  
    569     #for d in defines: print(d) 
    570     defines = '\n'.join('#define %s %s' % (k, v) for k, v in defines) 
    571     sources = '\n\n'.join(source) 
    572     return C_KERNEL_TEMPLATE % { 
    573         'DEFINES': defines, 
    574         'SOURCES': sources, 
    575         } 
    576  
    577 def categorize_parameters(pars): 
    578     """ 
    579     Build parameter categories out of the the parameter definitions. 
    580  
    581     Returns a dictionary of categories. 
    582  
    583     Note: these categories are subject to change, depending on the needs of 
    584     the UI and the needs of the kernel calling function. 
    585  
    586     The categories are as follows: 
    587  
    588     * *volume* list of volume parameter names 
    589     * *orientation* list of orientation parameters 
    590     * *magnetic* list of magnetic parameters 
    591     * *<empty string>* list of parameters that have no type info 
    592  
    593     Each parameter is in one and only one category. 
    594  
    595     The following derived categories are created: 
    596  
    597     * *fixed-1d* list of non-polydisperse parameters for 1D models 
    598     * *pd-1d* list of polydisperse parameters for 1D models 
    599     * *fixed-2d* list of non-polydisperse parameters for 2D models 
    600     * *pd-d2* list of polydisperse parameters for 2D models 
    601     """ 
    602     partype = { 
    603         'volume': [], 'orientation': [], 'magnetic': [], 'sld': [], '': [], 
    604         'fixed-1d': [], 'fixed-2d': [], 'pd-1d': [], 'pd-2d': [], 
    605         'pd-rel': set(), 
    606     } 
    607  
    608     for p in pars: 
    609         if p.type == 'volume': 
    610             partype['pd-1d'].append(p.name) 
    611             partype['pd-2d'].append(p.name) 
    612             partype['pd-rel'].add(p.name) 
    613         elif p.type == 'magnetic': 
    614             partype['fixed-2d'].append(p.name) 
    615         elif p.type == 'orientation': 
    616             partype['pd-2d'].append(p.name) 
    617         elif p.type in ('', 'sld'): 
    618             partype['fixed-1d'].append(p.name) 
    619             partype['fixed-2d'].append(p.name) 
    620         else: 
    621             raise ValueError("unknown parameter type %r" % p.type) 
    622         partype[p.type].append(p.name) 
    623  
    624     return partype 
    625  
    626 def process_parameters(model_info): 
    627     """ 
    628     Process parameter block, precalculating parameter details. 
    629     """ 
    630     # convert parameters into named tuples 
    631     for p in model_info['parameters']: 
    632         if p[4] == '' and (p[0].startswith('sld') or p[0].endswith('sld')): 
    633             p[4] = 'sld' 
    634             # TODO: make sure all models explicitly label their sld parameters 
    635             #raise ValueError("%s.%s needs to be explicitly set to type 'sld'" %(model_info['id'], p[0])) 
    636  
    637     pars = [Parameter(*p) for p in model_info['parameters']] 
    638     # Fill in the derived attributes 
    639     model_info['parameters'] = pars 
    640     partype = categorize_parameters(pars) 
    641     model_info['limits'] = dict((p.name, p.limits) for p in pars) 
    642     model_info['partype'] = partype 
    643     model_info['defaults'] = dict((p.name, p.default) for p in pars) 
    644     if model_info.get('demo', None) is None: 
    645         model_info['demo'] = model_info['defaults'] 
    646     model_info['has_2d'] = partype['orientation'] or partype['magnetic'] 
    647  
     457        pars = [qx, qy] + partable.iqxy_parameters 
     458        source.append(_gen_fn('Iqxy', pars, model_info['Iqxy'])) 
     459 
     460    # Define the parameter table 
     461    source.append("#define PARAMETER_TABLE \\") 
     462    source.append("\\\n".join(p.as_definition() 
     463                              for p in partable.kernel_parameters)) 
     464 
     465    # Define the function calls 
     466    if partable.form_volume_parameters: 
     467        refs = _call_pars("_v.", partable.form_volume_parameters) 
     468        call_volume = "#define CALL_VOLUME(_v) form_volume(%s)" % (",".join(refs)) 
     469    else: 
     470        # Model doesn't have volume.  We could make the kernel run a little 
     471        # faster by not using/transferring the volume normalizations, but 
     472        # the ifdef's reduce readability more than is worthwhile. 
     473        call_volume = "#define CALL_VOLUME(v) 1.0" 
     474    source.append(call_volume) 
     475 
     476    refs = ["_q[_i]"] + _call_pars("_v.", partable.iq_parameters) 
     477    call_iq = "#define CALL_IQ(_q,_i,_v) Iq(%s)" % (",".join(refs)) 
     478    if _have_Iqxy(user_code): 
     479        # Call 2D model 
     480        refs = ["q[2*i]", "q[2*i+1]"] + _call_pars("_v.", partable.iqxy_parameters) 
     481        call_iqxy = "#define CALL_IQ(_q,_i,_v) Iqxy(%s)" % (",".join(refs)) 
     482    else: 
     483        # Call 1D model with sqrt(qx^2 + qy^2) 
     484        warnings.warn("Creating Iqxy = Iq(sqrt(qx^2 + qy^2))") 
     485        # still defined:: refs = ["q[i]"] + _call_pars("v", iq_parameters) 
     486        pars_sqrt = ["sqrt(_q[2*_i]*_q[2*_i]+_q[2*_i+1]*_q[2*_i+1])"] + refs[1:] 
     487        call_iqxy = "#define CALL_IQ(_q,_i,_v) Iq(%s)" % (",".join(pars_sqrt)) 
     488 
     489    # Fill in definitions for numbers of parameters 
     490    source.append("#define MAX_PD %s"%partable.max_pd) 
     491    source.append("#define NPARS %d"%partable.npars) 
     492 
     493    # TODO: allow mixed python/opencl kernels? 
     494 
     495    # define the Iq kernel 
     496    source.append("#define KERNEL_NAME %s_Iq"%model_info['name']) 
     497    source.append(call_iq) 
     498    source.append(kernel_code) 
     499    source.append("#undef CALL_IQ") 
     500    source.append("#undef KERNEL_NAME") 
     501 
     502    # define the Iqxy kernel from the same source with different #defines 
     503    source.append("#define KERNEL_NAME %s_Iqxy"%model_info['name']) 
     504    source.append(call_iqxy) 
     505    source.append(kernel_code) 
     506    source.append("#undef CALL_IQ") 
     507    source.append("#undef KERNEL_NAME") 
     508 
     509    return '\n'.join(source) 
     510 
     511class CoordinationDetails(object): 
     512    def __init__(self, model_info): 
     513        parameters = model_info['parameters'] 
     514        max_pd = parameters.max_pd 
     515        npars = parameters.npars 
     516        par_offset = 4*max_pd 
     517        self.details = np.zeros(par_offset + 3*npars + 4, 'i4') 
     518 
     519        # generate views on different parts of the array 
     520        self._pd_par     = self.details[0*max_pd:1*max_pd] 
     521        self._pd_length  = self.details[1*max_pd:2*max_pd] 
     522        self._pd_offset  = self.details[2*max_pd:3*max_pd] 
     523        self._pd_stride  = self.details[3*max_pd:4*max_pd] 
     524        self._par_offset = self.details[par_offset+0*npars:par_offset+1*npars] 
     525        self._par_coord  = self.details[par_offset+1*npars:par_offset+2*npars] 
     526        self._pd_coord   = self.details[par_offset+2*npars:par_offset+3*npars] 
     527 
     528        # theta_par is fixed 
     529        self.details[-1] = parameters.theta_offset 
     530 
     531    @property 
     532    def ctypes(self): return self.details.ctypes 
     533 
     534    @property 
     535    def pd_par(self): return self._pd_par 
     536 
     537    @property 
     538    def pd_length(self): return self._pd_length 
     539 
     540    @property 
     541    def pd_offset(self): return self._pd_offset 
     542 
     543    @property 
     544    def pd_stride(self): return self._pd_stride 
     545 
     546    @property 
     547    def pd_coord(self): return self._pd_coord 
     548 
     549    @property 
     550    def par_coord(self): return self._par_coord 
     551 
     552    @property 
     553    def par_offset(self): return self._par_offset 
     554 
     555    @property 
     556    def num_active(self): return self.details[-4] 
     557    @num_active.setter 
     558    def num_active(self, v): self.details[-4] = v 
     559 
     560    @property 
     561    def total_pd(self): return self.details[-3] 
     562    @total_pd.setter 
     563    def total_pd(self, v): self.details[-3] = v 
     564 
     565    @property 
     566    def num_coord(self): return self.details[-2] 
     567    @num_coord.setter 
     568    def num_coord(self, v): self.details[-2] = v 
     569 
     570    @property 
     571    def theta_par(self): return self.details[-1] 
     572 
     573    def show(self): 
     574        print("total_pd", self.total_pd) 
     575        print("num_active", self.num_active) 
     576        print("pd_par", self.pd_par) 
     577        print("pd_length", self.pd_length) 
     578        print("pd_offset", self.pd_offset) 
     579        print("pd_stride", self.pd_stride) 
     580        print("par_offsets", self.par_offset) 
     581        print("num_coord", self.num_coord) 
     582        print("par_coord", self.par_coord) 
     583        print("pd_coord", self.pd_coord) 
     584        print("theta par", self.details[-1]) 
     585 
     586def mono_details(model_info): 
     587    details = CoordinationDetails(model_info) 
     588    # The zero defaults for monodisperse systems are mostly fine 
     589    details.par_offset[:] = np.arange(2, len(details.par_offset)+2) 
     590    return details 
     591 
     592def poly_details(model_info, weights): 
     593    #print("weights",weights) 
     594    weights = weights[2:] # Skip scale and background 
     595 
     596    # Decreasing list of polydispersity lengths 
     597    # Note: the reversing view, x[::-1], does not require a copy 
     598    pd_length = np.array([len(w) for w in weights]) 
     599    num_active = np.sum(pd_length>1) 
     600    if num_active > model_info['parameters'].max_pd: 
     601        raise ValueError("Too many polydisperse parameters") 
     602 
     603    pd_offset = np.cumsum(np.hstack((0, pd_length))) 
     604    idx = np.argsort(pd_length)[::-1][:num_active] 
     605    par_length = np.array([max(len(w),1) for w in weights]) 
     606    pd_stride = np.cumprod(np.hstack((1, par_length[idx]))) 
     607    par_offsets = np.cumsum(np.hstack((2, par_length))) 
     608 
     609    details = CoordinationDetails(model_info) 
     610    details.pd_par[:num_active] = idx 
     611    details.pd_length[:num_active] = pd_length[idx] 
     612    details.pd_offset[:num_active] = pd_offset[idx] 
     613    details.pd_stride[:num_active] = pd_stride[:-1] 
     614    details.par_offset[:] = par_offsets[:-1] 
     615    details.total_pd = pd_stride[-1] 
     616    details.num_active = num_active 
     617    # Without constraints coordinated parameters are just the pd parameters 
     618    details.par_coord[:num_active] = idx 
     619    details.pd_coord[:num_active] = 2**np.arange(num_active) 
     620    details.num_coord = num_active 
     621    #details.show() 
     622    return details 
     623 
     624def constrained_poly_details(model_info, weights, constraints): 
     625    # Need to find the independently varying pars and sort them 
     626    # Need to build a coordination list for the dependent variables 
     627    # Need to generate a constraints function which takes values 
     628    # and weights, returning par blocks 
     629    raise NotImplementedError("Can't handle constraints yet") 
     630 
     631 
     632def create_vector_Iq(model_info): 
     633    """ 
     634    Define Iq as a vector function if it exists. 
     635    """ 
     636    Iq = model_info['Iq'] 
     637    if callable(Iq) and not getattr(Iq, 'vectorized', False): 
     638        def vector_Iq(q, *args): 
     639            """ 
     640            Vectorized 1D kernel. 
     641            """ 
     642            return np.array([Iq(qi, *args) for qi in q]) 
     643        model_info['Iq'] = vector_Iq 
     644 
     645def create_vector_Iqxy(model_info): 
     646    """ 
     647    Define Iqxy as a vector function if it exists, or default it from Iq(). 
     648    """ 
     649    Iq, Iqxy = model_info['Iq'], model_info['Iqxy'] 
     650    if callable(Iqxy) and not getattr(Iqxy, 'vectorized', False): 
     651        def vector_Iqxy(qx, qy, *args): 
     652            """ 
     653            Vectorized 2D kernel. 
     654            """ 
     655            return np.array([Iqxy(qxi, qyi, *args) for qxi, qyi in zip(qx, qy)]) 
     656        model_info['Iqxy'] = vector_Iqxy 
     657    elif callable(Iq): 
     658        # Iq is vectorized because create_vector_Iq was already called. 
     659        def default_Iqxy(qx, qy, *args): 
     660            """ 
     661            Default 2D kernel. 
     662            """ 
     663            return Iq(np.sqrt(qx**2 + qy**2), *args) 
     664        model_info['Iqxy'] = default_Iqxy 
     665 
     666def create_default_functions(model_info): 
     667    """ 
     668    Autogenerate missing functions, such as Iqxy from Iq. 
     669 
     670    This only works for Iqxy when Iq is written in python. :func:`make_source` 
     671    performs a similar role for Iq written in C.  This also vectorizes 
     672    any functions that are not already marked as vectorized. 
     673    """ 
     674    create_vector_Iq(model_info) 
     675    create_vector_Iqxy(model_info)  # call create_vector_Iq() first 
    648676 
    649677def load_kernel_module(model_name): 
     
    682710      model variants (e.g., the list of cases) or is None if there are no 
    683711      model variants 
    684     * *defaults* is the *{parameter: value}* table built from the parameter 
    685       description table. 
    686     * *limits* is the *{parameter: [min, max]}* table built from the 
    687       parameter description table. 
    688     * *partypes* categorizes the model parameters. See 
     712    * *par_type* categorizes the model parameters. See 
    689713      :func:`categorize_parameters` for details. 
    690714    * *demo* contains the *{parameter: value}* map used in compare (and maybe 
     
    700724      *model_info* blocks for the composition objects.  This allows us to 
    701725      build complete product and mixture models from just the info. 
    702  
    703726    """ 
    704727    # TODO: maybe turn model_info into a class ModelDefinition 
    705     parameters = COMMON_PARAMETERS + kernel_module.parameters 
     728    #print("make parameter table", kernel_module.parameters) 
     729    parameters = make_parameter_table(kernel_module.parameters) 
    706730    filename = abspath(kernel_module.__file__) 
    707731    kernel_id = splitext(basename(filename))[0] 
     
    713737        filename=abspath(kernel_module.__file__), 
    714738        name=name, 
    715         title=kernel_module.title, 
    716         description=kernel_module.description, 
     739        title=getattr(kernel_module, 'title', name+" model"), 
     740        description=getattr(kernel_module, 'description', 'no description'), 
    717741        parameters=parameters, 
    718742        composition=None, 
     
    721745        single=getattr(kernel_module, 'single', True), 
    722746        structure_factor=getattr(kernel_module, 'structure_factor', False), 
     747        profile_axes=getattr(kernel_module, 'profile_axes', ['x','y']), 
    723748        variant_info=getattr(kernel_module, 'invariant_info', None), 
    724749        demo=getattr(kernel_module, 'demo', None), 
     
    726751        tests=getattr(kernel_module, 'tests', []), 
    727752        ) 
    728     process_parameters(model_info) 
     753    set_demo(model_info, getattr(kernel_module, 'demo', None)) 
    729754    # Check for optional functions 
    730     functions = "ER VR form_volume Iq Iqxy shape sesans".split() 
     755    functions = "ER VR form_volume Iq Iqxy profile sesans".split() 
    731756    model_info.update((k, getattr(kernel_module, k, None)) for k in functions) 
     757    create_default_functions(model_info) 
     758    # Precalculate the monodisperse parameters 
     759    # TODO: make this a lazy evaluator 
     760    # make_model_info is called for every model on sasview startup 
     761    model_info['mono_details'] = mono_details(model_info) 
    732762    return model_info 
    733763 
     
    796826    Program which prints the source produced by the model. 
    797827    """ 
     828    import sys 
    798829    if len(sys.argv) <= 1: 
    799830        print("usage: python -m sasmodels.generate modelname") 
Note: See TracChangeset for help on using the changeset viewer.