source: sasmodels/sasmodels/convert.py @ 88fafac

core_shell_microgelscostrafo411magnetic_modelticket-1257-vesicle-productticket_1156ticket_1265_superballticket_822_more_unit_tests
Last change on this file since 88fafac was 88fafac, checked in by krzywon, 7 years ago

#795: convert.py converts old style models to new style while keeping legacy funcitons in tact.

  • Property mode set to 100644
File size: 22.0 KB
Line 
1"""
2Convert models to and from sasview.
3"""
4from __future__ import print_function, division
5
6import math
7import warnings
8
9from .conversion_table import CONVERSION_TABLE
10from .core import load_model_info
11
12# List of models which SasView versions don't contain the explicit 'scale' argument.
13# When converting such a model, please update this list.
14MODELS_WITHOUT_SCALE = [
15    'teubner_strey',
16    'broad_peak',
17    'two_lorentzian',
18    "two_power_law",
19    'gauss_lorentz_gel',
20    'be_polyelectrolyte',
21    'correlation_length',
22    'fractal_core_shell',
23    'binary_hard_sphere',
24    'raspberry'
25]
26
27# List of models which SasView versions don't contain the explicit 'background' argument.
28# When converting such a model, please update this list.
29MODELS_WITHOUT_BACKGROUND = [
30    'guinier',
31]
32
33MODELS_WITHOUT_VOLFRACTION = [
34    'fractal',
35    'vesicle',
36    'multilayer_vesicle',
37]
38
39MAGNETIC_SASVIEW_MODELS = [
40    'core_shell',
41    'core_multi_shell',
42    'cylinder',
43    'parallelepiped',
44    'sphere',
45]
46
47
48# Convert new style names for polydispersity info to old style names
49PD_DOT = [
50    ("", ""),
51    ("_pd", ".width"),
52    ("_pd_n", ".npts"),
53    ("_pd_nsigma", ".nsigmas"),
54    ("_pd_type", ".type"),
55    ]
56
57CONVERT_OLD =[
58    "fittable",
59    "std",
60    "upper",
61    "lower",
62    "units"
63]
64
65def _rescale(par, scale):
66    return [pk*scale for pk in par] if isinstance(par, list) else par*scale
67
68def _is_sld(model_info, id):
69    """
70    Return True if parameter is a magnetic magnitude or SLD parameter.
71    """
72    if id.startswith('M0:'):
73        return True
74    if '_pd' in id or '.' in id:
75        return False
76    for p in model_info.parameters.call_parameters:
77        if p.id == id:
78            return p.type == 'sld'
79    # check through kernel parameters in case it is a named as a vector
80    for p in model_info.parameters.kernel_parameters:
81        if p.id == id:
82            return p.type == 'sld'
83    raise ValueError("unknown parameter %r in conversion"%id)
84
85def _rescale_sld(model_info, pars, scale):
86    """
87    rescale all sld parameters in the new model definition by *scale* so the
88    numbers are nicer.  Relies on the fact that all sld parameters in the
89    new model definition end with sld.  For backward conversion use
90    *scale=1e-6*.  For forward conversion use *scale=1e6*.
91    """
92    return dict((id, (_rescale(v, scale) if _is_sld(model_info, id) else v))
93                for id, v in pars.items())
94
95
96def _get_translation_table(model_info):
97    _, translation = CONVERSION_TABLE.get(model_info.id, [None, {}])
98    translation = translation.copy()
99    for p in model_info.parameters.kernel_parameters:
100        if p.length > 1:
101            newid = p.id
102            oldid = translation.get(p.id, p.id)
103            translation.pop(newid, None)
104            for k in range(1, p.length+1):
105                if newid+str(k) not in translation:
106                    translation[newid+str(k)] = oldid+str(k)
107    # Remove control parameter from the result
108    if model_info.control:
109        translation[model_info.control] = "CONTROL"
110    return translation
111
112# ========= FORWARD CONVERSION sasview 3.x => sasmodels ===========
113def _dot_pd_to_underscore_pd(par):
114    if par.endswith(".width"):
115        return par[:-6]+"_pd"
116    elif par.endswith(".type"):
117        return par[:-5]+"_pd_type"
118    elif par.endswith(".nsigmas"):
119        return par[:-8]+"_pd_nsigma"
120    elif par.endswith(".npts"):
121        return par[:-5]+"_pd_n"
122    else:
123        return par
124
125def _pd_to_underscores(pars):
126    return dict((_dot_pd_to_underscore_pd(k), v) for k, v in pars.items())
127
128def _convert_name(conv_dict, pars):
129    """
130    Renames parameter values (upper, lower, etc) to v4.0 names
131    :param conv_dict: conversion dictionary mapping new name : old name
132    :param pars: parameters to convert
133    :return:
134    """
135    import re
136    new_pars = {}
137    for key_par, value_par in pars.iteritems():
138        for key_conv, value_conv in conv_dict.iteritems():
139            if re.search(value_conv, key_par):
140                new_pars[key_par.replace(value_conv, key_conv)] = value_par
141                break
142            elif re.search("background", key_par) or re.search("scale", key_par):
143                new_pars[key_par] = value_par
144                break
145    return new_pars
146
147def _convert_pars(pars, mapping):
148    """
149    Rename the parameters and any associated polydispersity attributes.
150    """
151    newpars = pars.copy()
152    for new, old in mapping.items():
153        if old == new: continue
154        if old is None: continue
155        for underscore, dot in PD_DOT:
156            source = old+dot
157            if source in newpars:
158                if new is not None:
159                    target = new+dot
160                else:
161                    target = None
162                if source != target:
163                    if target:
164                        newpars[target] = pars[old+dot]
165                    del newpars[source]
166    return newpars
167
168
169def _conversion_target(model_name):
170    """
171    Find the sasmodel name which translates into the sasview name.
172
173    Note: *CoreShellEllipsoidModel* translates into *core_shell_ellipsoid:1*.
174    This is necessary since there is only one variant in sasmodels for the
175    two variants in sasview.
176    """
177    for sasmodels_name, [sasview_name, _] in CONVERSION_TABLE.items():
178        if sasview_name == model_name:
179            return sasmodels_name
180    return None
181
182
183def _hand_convert(name, oldpars):
184    if name == 'core_shell_parallelepiped':
185        # Make sure pd on rim parameters defaults to zero
186        # ... probably not necessary.
187        oldpars['rimA.width'] = 0.0
188        oldpars['rimB.width'] = 0.0
189        oldpars['rimC.width'] = 0.0
190    elif name == 'core_shell_ellipsoid:1':
191        # Reverse translation (from new to old), from core_shell_ellipsoid.c
192        #    equat_shell = equat_core + thick_shell
193        #    polar_core = equat_core * x_core
194        #    polar_shell = equat_core * x_core + thick_shell*x_polar_shell
195        # Forward translation (from old to new), inverting reverse translation:
196        #    thick_shell = equat_shell - equat_core
197        #    x_core = polar_core / equat_core
198        #    x_polar_shell = (polar_shell - polar_core)/(equat_shell - equat_core)
199        # Auto translation (old <=> new) happens after hand_convert
200        #    equat_shell <=> thick_shell
201        #    polar_core <=> x_core
202        #    polar_shell <=> x_polar_shell
203        # So...
204        equat_core, equat_shell = oldpars['equat_core'], oldpars['equat_shell']
205        polar_core, polar_shell = oldpars['polar_core'], oldpars['polar_shell']
206        oldpars['equat_shell'] = equat_shell - equat_core
207        oldpars['polar_core'] = polar_core / equat_core
208        oldpars['polar_shell'] = (polar_shell-polar_core)/(equat_shell-equat_core)
209    elif name == 'hollow_cylinder':
210        # now uses radius and thickness
211        thickness = oldpars['radius'] - oldpars['core_radius']
212        pd = oldpars['radius.width']*oldpars['radius']/thickness
213        oldpars['radius'] = thickness
214        oldpars['radius.width'] = pd
215    elif name == 'pearl_necklace':
216        pass
217        #_remove_pd(oldpars, 'num_pearls', name)
218        #_remove_pd(oldpars, 'thick_string', name)
219    elif name == 'polymer_micelle':
220        if 'ndensity' in oldpars:
221            oldpars['ndensity'] /= 1e15
222            oldpars['ndensity.lower'] /= 1e15
223            oldpars['ndensity.upper'] /= 1e15
224    elif name == 'rpa':
225        # convert scattering lengths from femtometers to centimeters
226        for p in "L1", "L2", "L3", "L4":
227            if p in oldpars:
228                oldpars[p] /= 1e-13
229                oldpars[p + ".lower"] /= 1e-13
230                oldpars[p + ".upper"] /= 1e-13
231    elif name == 'spherical_sld':
232        oldpars["CONTROL"] += 1
233    elif name == 'teubner_strey':
234        # basically undoing the entire Teubner-Strey calculations here.
235        #    drho = (sld_a - sld_b)
236        #    k = 2.0*math.pi*xi/d
237        #    a2 = (1.0 + k**2)**2
238        #    c1 = 2.0 * xi**2 * (1.0 - k**2)
239        #    c2 = xi**4
240        #    prefactor = 8.0*math.pi*phi*(1.0-phi)*drho**2*c2/xi
241        #    scale = 1e-4*prefactor
242        #    oldpars['scale'] = a2/scale
243        #    oldpars['c1'] = c1/scale
244        #    oldpars['c2'] = c2/scale
245
246        # need xi, d, sld_a, sld_b, phi=volfraction_a
247        # assume contrast is 1.0e-6, scale=1, background=0
248        sld_a, sld_b = 1.0, 0.
249        drho = sld_a - sld_b
250
251        # find xi
252        p_scale = oldpars['scale']
253        p_c1 = oldpars['c1']
254        p_c2= oldpars['c2']
255        xi = math.sqrt(2/(math.sqrt(p_scale/p_c2) + 0.5*p_c1/p_c2))
256
257        # find d from xi
258        k = math.sqrt(1 - 0.5*p_c1/p_c2*xi**2)
259        d = 2*math.pi*xi/k
260
261        # solve quadratic phi (1-phi) = xi/(1e-4 8 pi drho^2 c2)
262        # favour volume fraction in [0, 0.5]
263        c = xi / (1e-4 * 8.0 * math.pi * drho**2 * p_c2)
264        phi = 0.5 - math.sqrt(0.25 - c)
265
266        # scale sld_a by 1e-6 because the translator will scale it back
267        oldpars.update(volfraction_a=phi, xi=xi, d=d, sld_a=sld_a*1e-6,
268                       sld_b=sld_b, scale=1.0)
269        oldpars.pop('c1')
270        oldpars.pop('c2')
271
272    return oldpars
273
274def convert_model(name, pars, use_underscore=False):
275    """
276    Convert model from old style parameter names to new style.
277    """
278    newname = _conversion_target(name)
279    if newname is None:
280        return name, pars
281    if ':' in newname:   # core_shell_ellipsoid:1
282        model_info = load_model_info(newname[:-2])
283        # Know that the table exists and isn't multiplicity so grab it directly
284        # Can't use _get_translation_table since that will return the 'bare'
285        # version.
286        translation = CONVERSION_TABLE[newname]
287    else:
288        model_info = load_model_info(newname)
289        translation = _get_translation_table(model_info)
290    newpars = _convert_name(translation, pars.copy())
291    newpars = _hand_convert(newname, newpars)
292    newpars = _convert_pars(newpars, translation)
293    newpars = _rescale_sld(model_info, newpars, 1e6)
294    newpars.setdefault('scale', 1.0)
295    newpars.setdefault('background', 0.0)
296    if use_underscore:
297        newpars = _pd_to_underscores(newpars)
298    return newname, newpars
299
300
301# ========= BACKWARD CONVERSION sasmodels => sasview 3.x ===========
302
303def _revert_pars(pars, mapping):
304    """
305    Rename the parameters and any associated polydispersity attributes.
306    """
307    newpars = pars.copy()
308
309    for new, old in mapping.items():
310        for underscore, dot in PD_DOT:
311            if old and old+underscore == new+dot:
312                continue
313            if new+underscore in newpars:
314                if old is not None:
315                    newpars[old+dot] = pars[new+underscore]
316                del newpars[new+underscore]
317    for k in list(newpars.keys()):
318        for underscore, dot in PD_DOT[1:]:  # skip "" => ""
319            if k.endswith(underscore):
320                newpars[k[:-len(underscore)]+dot] = newpars[k]
321                del newpars[k]
322    return newpars
323
324def revert_name(model_info):
325    oldname, _ = CONVERSION_TABLE.get(model_info.id, [None, {}])
326    return oldname
327
328def _remove_pd(pars, key, name):
329    """
330    Remove polydispersity from the parameter list.
331
332    Note: operates in place
333    """
334    # Bumps style parameter names
335    width = pars.pop(key+".width", 0.0)
336    n_points = pars.pop(key+".npts", 0)
337    if width != 0.0 and n_points != 0:
338        warnings.warn("parameter %s not polydisperse in sasview %s"%(key, name))
339    pars.pop(key+".nsigmas", None)
340    pars.pop(key+".type", None)
341    return pars
342
343def _trim_vectors(model_info, pars, oldpars):
344    _, translation = CONVERSION_TABLE.get(model_info.id, [None, {}])
345    for p in model_info.parameters.kernel_parameters:
346        if p.length_control is not None:
347            n = int(pars[p.length_control])
348            oldname = translation.get(p.id, p.id)
349            for k in range(n+1, p.length+1):
350                for _, old in PD_DOT:
351                    oldpars.pop(oldname+str(k)+old, None)
352    return oldpars
353
354def revert_pars(model_info, pars):
355    """
356    Convert model from new style parameter names to old style.
357    """
358    if model_info.composition is not None:
359        composition_type, parts = model_info.composition
360        if composition_type == 'product':
361            translation = _get_translation_table(parts[0])
362            # structure factor models include scale:scale_factor mapping
363            translation.update(_get_translation_table(parts[1]))
364        else:
365            raise NotImplementedError("cannot convert to sasview sum")
366    else:
367        translation = _get_translation_table(model_info)
368    oldpars = _revert_pars(_rescale_sld(model_info, pars, 1e-6), translation)
369    oldpars = _trim_vectors(model_info, pars, oldpars)
370
371    # Make sure the control parameter is an integer
372    if "CONTROL" in oldpars:
373        oldpars["CONTROL"] = int(oldpars["CONTROL"])
374
375    # Note: update compare.constrain_pars to match
376    name = model_info.id
377    if name in MODELS_WITHOUT_SCALE or model_info.structure_factor:
378        if oldpars.pop('scale', 1.0) != 1.0:
379            warnings.warn("parameter scale not used in sasview %s"%name)
380    if name in MODELS_WITHOUT_BACKGROUND or model_info.structure_factor:
381        if oldpars.pop('background', 0.0) != 0.0:
382            warnings.warn("parameter background not used in sasview %s"%name)
383
384    # Remove magnetic parameters from non-magnetic sasview models
385    if name not in MAGNETIC_SASVIEW_MODELS:
386        oldpars = dict((k, v) for k, v in oldpars.items() if ':' not in k)
387
388    # If it is a product model P*S, then check the individual forms for special
389    # cases.  Note: despite the structure factor alone not having scale or
390    # background, the product model does, so this is below the test for
391    # models without scale or background.
392    namelist = name.split('*') if '*' in name else [name]
393    for name in namelist:
394        if name in MODELS_WITHOUT_VOLFRACTION:
395            del oldpars['volfraction']
396        elif name == 'core_multi_shell':
397            # kill extra shells
398            for k in range(5, 11):
399                oldpars.pop('sld_shell'+str(k), 0)
400                oldpars.pop('thick_shell'+str(k), 0)
401                oldpars.pop('mtheta:sld'+str(k), 0)
402                oldpars.pop('mphi:sld'+str(k), 0)
403                oldpars.pop('M0:sld'+str(k), 0)
404                _remove_pd(oldpars, 'sld_shell'+str(k), 'sld')
405                _remove_pd(oldpars, 'thick_shell'+str(k), 'thickness')
406        elif name == 'core_shell_parallelepiped':
407            _remove_pd(oldpars, 'rimA', name)
408            _remove_pd(oldpars, 'rimB', name)
409            _remove_pd(oldpars, 'rimC', name)
410        elif name == 'hollow_cylinder':
411            # now uses radius and thickness
412            thickness = oldpars['core_radius']
413            oldpars['radius'] += thickness
414            oldpars['radius.width'] *= thickness/oldpars['radius']
415        #elif name in ['mono_gauss_coil', 'poly_gauss_coil']:
416        #    del oldpars['i_zero']
417        elif name == 'onion':
418            oldpars.pop('n_shells', None)
419        elif name == 'pearl_necklace':
420            _remove_pd(oldpars, 'num_pearls', name)
421            _remove_pd(oldpars, 'thick_string', name)
422        elif name == 'polymer_micelle':
423            if 'ndensity' in oldpars:
424                oldpars['ndensity'] *= 1e15
425        elif name == 'rpa':
426            # convert scattering lengths from femtometers to centimeters
427            for p in "L1", "L2", "L3", "L4":
428                if p in oldpars: oldpars[p] *= 1e-13
429            if pars['case_num'] < 2:
430                for k in ("a", "b"):
431                    for p in ("L", "N", "Phi", "b", "v"):
432                        oldpars.pop(p+k, None)
433                for k in "Kab,Kac,Kad,Kbc,Kbd".split(','):
434                    oldpars.pop(k, None)
435            elif pars['case_num'] < 5:
436                for k in ("a",):
437                    for p in ("L", "N", "Phi", "b", "v"):
438                        oldpars.pop(p+k, None)
439                for k in "Kab,Kac,Kad".split(','):
440                    oldpars.pop(k, None)
441        elif name == 'spherical_sld':
442            oldpars["CONTROL"] -= 1
443            # remove polydispersity from shells
444            for k in range(1, 11):
445                _remove_pd(oldpars, 'thick_flat'+str(k), 'thickness')
446                _remove_pd(oldpars, 'thick_inter'+str(k), 'interface')
447            # remove extra shells
448            for k in range(int(pars['n_shells']), 11):
449                oldpars.pop('sld_flat'+str(k), 0)
450                oldpars.pop('thick_flat'+str(k), 0)
451                oldpars.pop('thick_inter'+str(k), 0)
452                oldpars.pop('func_inter'+str(k), 0)
453                oldpars.pop('nu_inter'+str(k), 0)
454        elif name == 'stacked_disks':
455            _remove_pd(oldpars, 'n_stacking', name)
456        elif name == 'teubner_strey':
457            # basically redoing the entire Teubner-Strey calculations here.
458            volfraction = oldpars.pop('volfraction_a')
459            xi = oldpars.pop('xi')
460            d = oldpars.pop('d')
461            sld_a = oldpars.pop('sld_a')
462            sld_b = oldpars.pop('sld_b')
463            drho = 1e6*(sld_a - sld_b)  # conversion autoscaled these
464            k = 2.0*math.pi*xi/d
465            a2 = (1.0 + k**2)**2
466            c1 = 2.0 * xi**2 * (1.0 - k**2)
467            c2 = xi**4
468            prefactor = 8.0*math.pi*volfraction*(1.0-volfraction)*drho**2*c2/xi
469            scale = 1e-4*prefactor
470            oldpars['scale'] = a2/scale
471            oldpars['c1'] = c1/scale
472            oldpars['c2'] = c2/scale
473
474    #print("convert from",list(sorted(pars)))
475    #print("convert to",list(sorted(oldpars.items())))
476    return oldpars
477
478def constrain_new_to_old(model_info, pars):
479    """
480    Restrict parameter values to those that will match sasview.
481    """
482    name = model_info.id
483    # Note: update convert.revert_model to match
484    if name in MODELS_WITHOUT_SCALE or model_info.structure_factor:
485        pars['scale'] = 1
486    if name in MODELS_WITHOUT_BACKGROUND or model_info.structure_factor:
487        pars['background'] = 0
488    # sasview multiplies background by structure factor
489    if '*' in name:
490        pars['background'] = 0
491
492    # Shut off magnetism when comparing non-magnetic sasview models
493    if name not in MAGNETIC_SASVIEW_MODELS:
494        suppress_magnetism = False
495        for key in pars.keys():
496            if key.startswith("M0:"):
497                suppress_magnetism = suppress_magnetism or (pars[key] != 0)
498                pars[key] = 0
499        if suppress_magnetism:
500            warnings.warn("suppressing magnetism for comparison with sasview")
501
502    # Shut off theta polydispersity since algorithm has changed
503    if 'theta_pd_n' in pars:
504        if pars['theta_pd_n'] != 0:
505            warnings.warn("suppressing theta polydispersity for comparison with sasview")
506        pars['theta_pd_n'] = 0
507
508    # If it is a product model P*S, then check the individual forms for special
509    # cases.  Note: despite the structure factor alone not having scale or
510    # background, the product model does, so this is below the test for
511    # models without scale or background.
512    namelist = name.split('*') if '*' in name else [name]
513    for name in namelist:
514        if name in MODELS_WITHOUT_VOLFRACTION:
515            pars['volfraction'] = 1
516        if name == 'core_multi_shell':
517            pars['n'] = min(math.ceil(pars['n']), 4)
518        elif name == 'gel_fit':
519            pars['scale'] = 1
520        elif name == 'line':
521            pars['scale'] = 1
522            pars['background'] = 0
523        elif name == 'mono_gauss_coil':
524            pars['scale'] = 1
525        elif name == 'onion':
526            pars['n_shells'] = math.ceil(pars['n_shells'])
527        elif name == 'pearl_necklace':
528            pars['string_thickness_pd_n'] = 0
529            pars['number_of_pearls_pd_n'] = 0
530        elif name == 'poly_gauss_coil':
531            pars['scale'] = 1
532        elif name == 'rpa':
533            pars['case_num'] = int(pars['case_num'])
534        elif name == 'spherical_sld':
535            pars['n_shells'] = math.ceil(pars['n_shells'])
536            pars['n_steps'] = math.ceil(pars['n_steps'])
537            for k in range(1, 11):
538                pars['shape%d'%k] = math.trunc(pars['shape%d'%k]+0.5)
539            for k in range(2, 11):
540                pars['thickness%d_pd_n'%k] = 0
541                pars['interface%d_pd_n'%k] = 0
542        elif name == 'teubner_strey':
543            pars['scale'] = 1
544            if pars['volfraction_a'] > 0.5:
545                pars['volfraction_a'] = 1.0 - pars['volfraction_a']
546        elif name == 'unified_power_Rg':
547            pars['level'] = int(pars['level'])
548
549def _check_one(name, seed=None):
550    """
551    Generate a random set of parameters for *name*, and check that they can
552    be converted back to SasView 3.x and forward again to sasmodels.  Raises
553    an error if the parameters are changed.
554    """
555    from . import compare
556
557    model_info = load_model_info(name)
558
559    old_name = revert_name(model_info)
560    if old_name is None:
561        return
562
563    pars = compare.get_pars(model_info, use_demo=False)
564    pars = compare.randomize_pars(model_info, pars, seed=seed)
565    if name == "teubner_strey":
566        # T-S model is underconstrained, so fix the assumptions.
567        pars['sld_a'], pars['sld_b'] = 1.0, 0.0
568    compare.constrain_pars(model_info, pars)
569    constrain_new_to_old(model_info, pars)
570    old_pars = revert_pars(model_info, pars)
571    new_name, new_pars = convert_model(old_name, old_pars, use_underscore=True)
572    if 1:
573        print("==== %s in ====="%name)
574        print(str(compare.parlist(model_info, pars, True)))
575        print("==== %s ====="%old_name)
576        for k, v in sorted(old_pars.items()):
577            print(k, v)
578        print("==== %s out ====="%new_name)
579        print(str(compare.parlist(model_info, new_pars, True)))
580    assert name==new_name, "%r != %r"%(name, new_name)
581    for k, v in new_pars.items():
582        assert k in pars, "%s: %r appeared from conversion"%(name, k)
583        if isinstance(v, float):
584            assert abs(v-pars[k])<=abs(1e-12*v), "%s: %r  %s != %s"%(name, k, v, pars[k])
585        else:
586            assert v == pars[k], "%s: %r  %s != %s"%(name, k, v, pars[k])
587    for k, v in pars.items():
588        assert k in pars, "%s: %r not converted"%(name, k)
589
590def test_backward_forward():
591    from .core import list_models
592    for name in list_models('all'):
593        L = lambda: _check_one(name, seed=1)
594        L.description = name
595        yield L
Note: See TracBrowser for help on using the repository browser.