Changeset 3543141 in sasmodels for sasmodels/generate.py
- Timestamp:
- Apr 7, 2016 2:06:03 PM (8 years ago)
- 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. - File:
-
- 1 edited
Legend:
- Unmodified
- Added
- Removed
-
sasmodels/generate.py
re6408d0 r6e7ff6d 21 21 22 22 *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. 23 27 24 28 These functions are defined in a kernel module .py script and an associated … … 65 69 The constructor code will generate the necessary vectors for computing 66 70 them with the desired polydispersity. 67 68 The available kernel parameters are defined as a list, with each parameter69 defined as a sublist with the following elements:70 71 *name* is the name that will be used in the call to the kernel72 function and the name that will be displayed to the user. Names73 should be lower case, with words separated by underscore. If74 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 it80 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 to83 limit the polydispersity density function. In the fit, the parameter limits84 given to the fit are the limits on the central value of the parameter.85 If there is polydispersity, it will evaluate parameter values outside86 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" parameters90 will be used in all functions. "orientation" parameters will be used91 in *Iqxy* and *Imagnetic*. "magnetic* parameters will be used in92 *Imagnetic* only. If *type* is the empty string, the parameter will93 be used in all of *Iq*, *Iqxy* and *Imagnetic*. "sld" parameters94 can automatically be promoted to magnetic parameters, each of which95 will have a magnitude and a direction, which may be different from96 other sld parameters.97 98 *description* is a short description of the parameter. This will99 be displayed in the parameter table and used as a tool tip for the100 parameter value in the user interface.101 102 71 The kernel module must set variables defining the kernel meta data: 103 72 … … 156 125 An *model_info* dictionary is constructed from the kernel meta data and 157 126 returned 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 for161 the non-polydisperse parameters, followed by the lengths of the162 polydispersity loops. To construct the call for 1D models, the163 categories *fixed-1d* and *pd-1d* list the names of the parameters164 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 give167 polydispersitiy as a portion of the value (so a 10% length dispersity168 would use a polydispersity value of 0.1) rather than absolute169 dispersity such as an angle plus or minus 15 degrees.170 171 The *volume* category lists the volume parameters in order for calls172 to volume within the kernel (used for volume normalization) and for173 calls to ER and VR for effective radius and volume ratio respectively.174 175 The *orientation* and *magnetic* categories list the orientation and176 magnetic parameters. These are used by the sasview interface. The177 blank category is for parameters such as scale which don't have any178 other marking.179 127 180 128 The doc string at the start of the kernel module will be used to … … 204 152 from __future__ import print_function 205 153 206 #TODO: identify model files which have changed since loading and reload them.207 154 #TODO: determine which functions are useful outside of generate 208 155 #__all__ = ["model_info", "make_doc", "make_source", "convert_type"] 209 156 210 import sys211 157 from os.path import abspath, dirname, join as joinpath, exists, basename, \ 212 splitext 158 splitext, getmtime 213 159 import re 214 160 import string 215 161 import warnings 216 from collections import namedtuple217 162 218 163 import numpy as np 219 164 165 from .modelinfo import ModelInfo, Parameter, make_parameter_table, set_demo 220 166 from .custom import load_custom_kernel_module 221 167 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') 168 TEMPLATE_ROOT = dirname(__file__) 226 169 227 170 F16 = np.dtype('float16') … … 232 175 except TypeError: 233 176 F128 = None 234 235 # Scale and background, which are parameters common to every form factor236 COMMON_PARAMETERS = [237 ["scale", "", 1, [0, np.inf], "", "Source intensity"],238 ["background", "1/cm", 1e-3, [0, np.inf], "", "Source background"],239 ]240 177 241 178 # Conversion from units defined in the parameter table for each model … … 331 268 raise ValueError("%r not found in %s" % (filename, search_path)) 332 269 270 333 271 def model_sources(model_info): 334 272 """ … … 339 277 return [_search(search_path, f) for f in model_info['source']] 340 278 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 """ 279 def 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 354 289 355 290 def convert_type(source, dtype): … … 362 297 if dtype == F16: 363 298 fbytes = 2 364 source = _ F16_PRAGMA + _convert_type(source, "half", "f")299 source = _convert_type(source, "half", "f") 365 300 elif dtype == F32: 366 301 fbytes = 4 … … 368 303 elif dtype == F64: 369 304 fbytes = 8 370 source = _F64_PRAGMA + source # Sourceis already double305 # no need to convert if it is already double 371 306 elif dtype == F128: 372 307 fbytes = 16 … … 411 346 412 347 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 = {} 349 def 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 357 def 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 = """\ 365 double %(name)s(%(pars)s); 366 double %(name)s(%(pars)s) { 367 %(body)s 368 } 369 370 417 371 """ 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 373 def _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 387 def _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) 395 def _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 437 416 def make_source(model_info): 438 417 """ … … 453 432 # for computing volume even if we allow non-disperse volume parameters. 454 433 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 490 450 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'])) 523 453 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'])) 552 456 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 511 class 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 586 def 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 592 def 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 624 def 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 632 def 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 645 def 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 666 def 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 648 676 649 677 def load_kernel_module(model_name): … … 682 710 model variants (e.g., the list of cases) or is None if there are no 683 711 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 689 713 :func:`categorize_parameters` for details. 690 714 * *demo* contains the *{parameter: value}* map used in compare (and maybe … … 700 724 *model_info* blocks for the composition objects. This allows us to 701 725 build complete product and mixture models from just the info. 702 703 726 """ 704 727 # 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) 706 730 filename = abspath(kernel_module.__file__) 707 731 kernel_id = splitext(basename(filename))[0] … … 713 737 filename=abspath(kernel_module.__file__), 714 738 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'), 717 741 parameters=parameters, 718 742 composition=None, … … 721 745 single=getattr(kernel_module, 'single', True), 722 746 structure_factor=getattr(kernel_module, 'structure_factor', False), 747 profile_axes=getattr(kernel_module, 'profile_axes', ['x','y']), 723 748 variant_info=getattr(kernel_module, 'invariant_info', None), 724 749 demo=getattr(kernel_module, 'demo', None), … … 726 751 tests=getattr(kernel_module, 'tests', []), 727 752 ) 728 process_parameters(model_info)753 set_demo(model_info, getattr(kernel_module, 'demo', None)) 729 754 # 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() 731 756 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) 732 762 return model_info 733 763 … … 796 826 Program which prints the source produced by the model. 797 827 """ 828 import sys 798 829 if len(sys.argv) <= 1: 799 830 print("usage: python -m sasmodels.generate modelname")
Note: See TracChangeset
for help on using the changeset viewer.