Source code for GSASIImapvars

# -*- coding: utf-8 -*-
########### SVN repository information ###################
# $Date: 2015-02-28 12:46:37 +0900 (Sat, 28 Feb 2015) $
# $Author: toby $
# $Revision: 1676 $
# $URL: https://subversion.xray.aps.anl.gov/pyGSAS/trunk/GSASIImapvars.py $
# $Id: GSASIImapvars.py 1676 2015-02-28 03:46:37Z toby $
########### SVN repository information ###################
"""
*GSASIImapvars: Parameter constraints*
======================================

Module to implements algebraic contraints, parameter redefinition
and parameter simplification contraints.

Parameter redefinition (new vars) is done by creating one or more relationships
between a set of parameters

::

   Mx1 * Px + My1 * Py +...
   Mx2 * Px + Mz2 * Pz + ...

where Pj is a parameter name and Mjk is a constant.

Constant constraint Relations can also be supplied in the form of an equation:

::

  nx1 * Px + ny1 * Py +... = C1

where Cn is a constant. These equations define an algebraic
constrant.

Parameters can also be "fixed" (held), which prevents them from being refined.

All of the above three cases are input using routines
GroupConstraints and GenerateConstraints. The input consists of a list of
relationship dictionaries:

.. code-block:: python

    constrDict = [
         {'0:12:Scale': 2.0, '0:14:Scale': 4.0, '0:13:Scale': 3.0, '0:0:Scale': 0.5},
         {'2::C(10,6,1)': 1.0, '1::C(10,6,1)': 1.0},
         {'0::A0': 0.0}]
    fixedList = ['5.0', None, '0']

Where the dictionary defines the first part of an expression and the corresponding fixedList
item is either None (for parameter redefinition) or the constant value, for a constant
constraint equation. A dictionary that contains a single term defines a variable that
will be fixed (held). The multiplier and the fixedList value in this case are ignored.

Parameters can also be equivalenced or "slaved" to another parameter, such that one
(independent) parameter is equated to several (now dependent) parameters. In
algebraic form this is:

::

   P0 = M1 * P1 = M2 * P2 = ...

Thus parameters P0, P1 and P2,... are linearly equivalent. Routine StoreEquivalence is
used to specify these equivalences.

Parameter redefinition (new vars) describes a new, independent, parameter, which
is defined in terms of dependent parameters that are defined in the
Model, while fixed constrained relations effectively reduce the complexity
of the Model by removing a degree of freedom. It is possible for a parameter to appear
in both a parameter redefinition expression and a fixed constraint equation, but a
parameter cannot be used a parameter equivalance cannot be used elsewhere (not fixed,
constrained or redefined). Likewise a fixed parameter cannot be used elsewhere (not
equivalanced, constrained or redefined).

Relationships are grouped so that a set of dependent parameters appear
in only one group (done in routine GroupConstraints.) Note that if a
group contains relationships/equations that involve N dependent
parameters, there must exist N-C new parameters, where C is the number
of contraint equations in the group. Routine GenerateConstraints takes
the output from GroupConstraints and generates the
"missing" relationships and saves that information in the module's
global variables. Each generated parameter is named sequentially using paramPrefix.

A list of parameters that will be varied is specified as input to GenerateConstraints
(varyList). A fixed parameter will simply be removed from this list preventing that
parameter from being varied. Note that all parameters in a constraint relationship
must specified as varied (appear in varyList) or none can be varied. This is
checked in GenerateConstraints. Likewise, if all parameters in a constraint are
not referenced in a refinement, the constraint is ignored, but if some parameters
in a constraint group are not referenced in a refinement, but others are this
constitutes and error. 

* When a new variable is created, the variable is assigned the name associated
  in the constraint definition or it is assigned a default name of form
  ``::constr<n>`` (see paramPrefix). The vary setting for variables used in the
  constraint are ignored.
  Note that any generated "missing" relations are not varied. Only
  the input relations can be are varied.
  
* If all parameters in a fixed constraint equation are varied, the generated "missing"
  relations in the group are all varied. This provides the N-C degrees of freedom. 

*External Routines*
-------------------

To define a set of constrained and unconstrained relations, one
defines a list of dictionary defining constraint parameters and their
values, a list of fixed values for each constraint and a list of
parameters to be varied. In addition, one uses
:func:`StoreEquivalence` to define parameters that are equivalent. One
can then use :func:`CheckConstraints` to check that the input is
internally consistent and finally :func:`GroupConstraints` and
:func:`GenerateConstraints` to generate the internally used
tables. Routines :func:`Map2Dict` is used to initialize the parameter
dictionary and :func:`Dict2Map`, :func:`Dict2Deriv`, and
:func:`ComputeDepESD` are used to apply constraints. Routine
:func:`VarRemapShow` is used to print out the constraint information,
as stored by :func:`GenerateConstraints`.

:func:`InitVars`
  This is optionally used to clear out all defined previously defined constraint information
  
:func:`StoreEquivalence`
  To implement parameter redefinition, one calls StoreEquivalence. This should be called for every set of
  equivalence relationships. There is no harm in using StoreEquivalence with the same independent variable:

  .. code-block:: python

       StoreEquivalence('x',('y',))
       StoreEquivalence('x',('z',))

  or equivalently 

  .. code-block:: python

       StoreEquivalence('x',('y','z'))

  The latter will run more efficiently. Note that mixing independent and dependent variables is
  problematic. This is not allowed:

  .. code-block:: python

        StoreEquivalence('x',('y',))
        StoreEquivalence('y',('z',))
        
  Use StoreEquivalence before calling GenerateConstraints or CheckConstraints

:func:`CheckConstraints`
   To check that input in internally consistent, use CheckConstraints

:func:`Map2Dict`
   To determine values for the parameters created in this module, one
   calls Map2Dict. This will not apply contraints.

:func:`Dict2Map`
   To take values from the new independent parameters and constraints,
   one calls Dict2Map. This will apply contraints.

:func:`Dict2Deriv`
   Use Dict2Deriv to determine derivatives on independent parameters
   from those on dependent ones

:func:`ComputeDepESD`      
   Use ComputeDepESD to compute uncertainties on dependent variables

:func:`VarRemapShow`
   To show a summary of the parameter remapping, one calls VarRemapShow

*Global Variables*
------------------

dependentParmList:
   contains a list by group of lists of
   parameters used in the group. Note that parameters listed in
   dependentParmList should not be refined as they will not affect
   the model

indParmList:
     a list of groups of Independent parameters defined in
     each group. This contains both parameters used in parameter
     redefinitions as well as names of generated new parameters.

fixedVarList:
     a list of variables that have been 'fixed'
     by defining them as equal to a constant (::var: = 0). Note that
     the constant value is ignored at present. These variables are
     later removed from varyList which prevents them from being refined. 
     Unlikely to be used externally.

arrayList:
     a list by group of relationship matrices to relate
     parameters in dependentParmList to those in indParmList. Unlikely
     to be used externally.

invarrayList:
     a list by group of relationship matrices to relate
     parameters in indParmList to those in dependentParmList. Unlikely
     to be used externally.

fixedDict:
     a dictionary containing the fixed values corresponding
     to parameter equations.  The dict key is an ascii string, but the
     dict value is a float.  Unlikely to be used externally.

*Routines*
----------

Note that parameter names in GSAS-II are strings of form ``<ph>:<hst>:<nam>``

"""

import numpy as np
import GSASIIpath
GSASIIpath.SetVersionNumber("$Revision: 1676 $")
# data used for constraints; 
debug = False # turns on printing as constraint input is processed
# note that constraints are stored listed by contraint groups, where each constraint
# group contains those parameters that must be handled together
dependentParmList = [] # contains a list of parameters in each group
# note that parameters listed in dependentParmList should not be refined 
arrayList = [] # a list of of relationship matrices 
invarrayList = [] # a list of inverse relationship matrices 
indParmList = [] # a list of names for the new parameters
fixedDict = {} # a dictionary containing the fixed values corresponding to defined parameter equations
               # key is original ascii string, value is float
fixedVarList = [] # List of variables that should not be refined

# prefix for parameter names
paramPrefix = "::constr"
consNum = 0 # number of the next constraint to be created

[docs]def InitVars(): '''Initializes all constraint information''' global dependentParmList,arrayList,invarrayList,indParmList,fixedDict,consNum dependentParmList = [] # contains a list of parameters in each group arrayList = [] # a list of of relationship matrices invarrayList = [] # a list of inverse relationship matrices indParmList = [] # a list of names for the new parameters fixedDict = {} # a dictionary containing the fixed values corresponding to defined parameter equations consNum = 0 # number of the next constraint to be created fixedVarList = []
[docs]def VarKeys(constr): """Finds the keys in a constraint that represent variables e.g. eliminates any that start with '_' :param dict constr: a single constraint entry of form:: {'var1': mult1, 'var2': mult2,... '_notVar': val,...} (see :func:`GroupConstraints`) :returns: a list of keys where any keys beginning with '_' are removed. """ return [i for i in constr.keys() if not i.startswith('_')]
[docs]def GroupConstraints(constrDict): """divide the constraints into groups that share no parameters. :param dict constrDict: a list of dicts defining relationships/constraints :: constrDict = [{<constr1>}, {<constr2>}, ...] where {<constr1>} is {'var1': mult1, 'var2': mult2,... } :returns: two lists of lists: * a list of grouped contraints where each constraint grouped containts a list of indices for constraint constrDict entries * a list containing lists of parameter names contained in each group """ assignedlist = [] # relationships that have been used groups = [] # contains a list of grouplists ParmList = [] for i,consi in enumerate(constrDict): if i in assignedlist: continue # already in a group, skip # starting a new group grouplist = [i,] assignedlist.append(i) groupset = set(VarKeys(consi)) changes = True # always loop at least once while(changes): # loop until we can't find anything to add to the current group changes = False # but don't loop again unless we find something for j,consj in enumerate(constrDict): if j in assignedlist: continue # already in a group, skip if len(set(VarKeys(consj)) & groupset) > 0: # true if this needs to be added changes = True grouplist.append(j) assignedlist.append(j) groupset = groupset | set(VarKeys(consj)) group = sorted(grouplist) varlist = sorted(list(groupset)) groups.append(group) ParmList.append(varlist) return groups,ParmList
[docs]def CheckConstraints(varyList,constrDict,fixedList): '''Takes a list of relationship entries comprising a group of constraints and checks for inconsistencies such as conflicts in parameter/variable definitions and or inconsistently varied parameters. :param list varyList: a list of parameters names that will be varied :param dict constrDict: a list of dicts defining relationships/constraints (as created in :func:`GSASIIstrIO.ProcessConstraints` and documented in :func:`GroupConstraints`) :param list fixedList: a list of values specifying a fixed value for each dict in constrDict. Values are either strings that can be converted to floats or ``None`` if the constraint defines a new parameter rather than a constant. :returns: two strings: * the first lists conflicts internal to the specified constraints * the second lists conflicts where the varyList specifies some parameters in a constraint, but not all If there are no errors, both strings will be empty ''' import re global dependentParmList,arrayList,invarrayList,indParmList,consNum errmsg = '' warnmsg = '' fixVlist = [] # process fixed variables (holds) for cdict in constrDict: # N.B. No "_" names in holds if len(cdict) == 1: fixVlist.append(cdict.keys()[0]) # process equivalences: make a list of dependent and independent vars # and check for repeated uses (repetition of a parameter as an # independent var is OK) indepVarList = [] depVarList = [] multdepVarList = [] for varlist,mapvars,multarr,invmultarr in zip( dependentParmList,indParmList,arrayList,invarrayList): if multarr is None: # an equivalence zeromult = False for mv in mapvars: varied = 0 notvaried = '' if mv in varyList: varied += 1 else: if notvaried: notvaried += ', ' notvaried += mv if mv not in indepVarList: indepVarList.append(mv) for v,m in zip(varlist,invmultarr): if m == 0: zeromult = True if v in varyList: varied += 1 else: if notvaried: notvaried += ', ' notvaried += v if v in depVarList: multdepVarList.append(v) else: depVarList.append(v) if varied > 0 and varied != len(varlist)+1: warnmsg += "\nNot all variables refined in equivalence:\n\t" s = "" for v in varlist: if s != "": s+= " & " s += str(v) warnmsg += str(mv) + " => " + s warnmsg += '\nNot refined: ' + notvaried + '\n' if zeromult: errmsg += "\nZero multiplier is invalid in equivalence:\n\t" s = "" for v in varlist: if s != "": s+= " & " s += str(v) errmsg += str(mv) + " => " + s + '\n' # check for errors: inboth = set(indepVarList).intersection(set(depVarList)) if len(inboth) > 0: errmsg += "\nThe following parameters(s) are used as both dependent and independent variables in Equivalence relations:\n" s = '' for var in sorted(inboth): if s != "": s+= ", " s += str(var) errmsg += '\t'+ s + '\n' if len(multdepVarList) > 0: errmsg += "\nThe following parameters(s) are used in multiple Equivalence relations as dependent variables:\n" s = '' for var in sorted(set(multdepVarList)): if s != "": s+= ", " s += str(var) errmsg += '\t'+ s + '\n' equivVarList = list(set(indepVarList).union(set(depVarList))) if debug: print 'equivVarList',equivVarList inboth = set(fixVlist).intersection(set(equivVarList)) if len(inboth) > 0: errmsg += "\nThe following parameter(s) are used in both Equivalence and Fixed constraints:\n" s = '' for var in sorted(inboth): if s != "": s+= ", " s += str(var) errmsg += '\t'+ s + '\n' groups,parmlist = GroupConstraints(constrDict) # scan through parameters in each relationship. Are all varied? If only some are # varied, create a warning message. for group,varlist in zip(groups,parmlist): if len(varlist) == 1: continue for rel in group: varied = 0 notvaried = '' for var in constrDict[rel]: if var.startswith('_'): continue if not re.match('[0-9]*:[0-9\*]*:',var): warnmsg += "\nVariable "+str(var)+" does not begin with a ':'" if var in varyList: varied += 1 else: if notvaried: notvaried += ', ' notvaried += var if var in fixVlist: errmsg += '\nParameter '+var+" is Fixed and used in a constraint:\n\t" errmsg += _FormatConstraint(constrDict[rel],fixedList[rel])+"\n" if varied > 0 and varied != len(VarKeys(constrDict[rel])): warnmsg += "\nNot all variables refined in constraint:\n\t" warnmsg += _FormatConstraint(constrDict[rel],fixedList[rel]) warnmsg += '\nNot refined: ' + notvaried + '\n' if errmsg or warnmsg: return errmsg,warnmsg # now look for process each group and create the relations that are needed to form # non-singular square matrix for group,varlist in zip(groups,parmlist): if len(varlist) == 1: continue # a constraint group with a single variable can be ignored if len(varlist) < len(group): # too many relationships -- no can do errmsg += "\nOver-constrained input. " errmsg += "There are more constraints " + str(len(group)) errmsg += "\n\tthan variables " + str(len(varlist)) + "\n" for rel in group: errmsg += _FormatConstraint(constrDict[rel],fixedList[rel]) errmsg += "\n" continue try: multarr = _FillArray(group,constrDict,varlist) _RowEchelon(len(group),multarr,varlist) except: errmsg += "\nSingular input. " errmsg += "There are internal inconsistencies in these constraints\n" for rel in group: errmsg += _FormatConstraint(constrDict[rel],fixedList[rel]) errmsg += "\n" continue try: multarr = _FillArray(group,constrDict,varlist,FillDiagonals=True) GramSchmidtOrtho(multarr,len(group)) except: errmsg += "\nUnexpected singularity with constraints (in Gram-Schmidt)\n" for rel in group: errmsg += _FormatConstraint(constrDict[rel],fixedList[rel]) errmsg += "\n" continue mapvar = [] group = group[:] # scan through all generated and input variables # Check again for inconsistent variable use # for new variables -- where varied and unvaried parameters get grouped # together. I don't think this can happen when not flagged before, but # it does not hurt to check again. for i in range(len(varlist)): varied = 0 notvaried = '' if len(group) > 0: rel = group.pop(0) fixedval = fixedList[rel] for var in VarKeys(constrDict[rel]): if var in varyList: varied += 1 else: if notvaried: notvaried += ', ' notvaried += var else: fixedval = None if fixedval is None: varname = paramPrefix + str(consNum) # assign a name to a variable mapvar.append(varname) consNum += 1 else: mapvar.append(fixedval) if varied > 0 and notvaried != '': warnmsg += "\nNot all variables refined in generated constraint" warnmsg += '\nPlease report this unexpected error\n' for rel in group: warnmsg += _FormatConstraint(constrDict[rel],fixedList[rel]) warnmsg += "\n" warnmsg += '\n\tNot refined: ' + notvaried + '\n' try: np.linalg.inv(multarr) except: errmsg += "\nSingular input. " errmsg += "The following constraints are not " errmsg += "linearly independent\n\tor do not " errmsg += "allow for generation of a non-singular set\n" errmsg += 'Please report this unexpected error\n' for rel in group: errmsg += _FormatConstraint(constrDict[rel],fixedList[rel]) errmsg += "\n" return errmsg,warnmsg
[docs]def GenerateConstraints(groups,parmlist,varyList,constrDict,fixedList,parmDict=None,SeqHist=None): '''Takes a list of relationship entries comprising a group of constraints and builds the relationship lists and their inverse and stores them in global variables Also checks for internal conflicts or inconsistencies in parameter/variable definitions. :param list groups: a list of grouped contraints where each constraint grouped containts a list of indices for constraint constrDict entries, created in :func:`GroupConstraints` (returned as 1st value) :param list parmlist: a list containing lists of parameter names contained in each group, created in :func:`GroupConstraints` (returned as 2nd value) :param list varyList: a list of parameters names (strings of form ``<ph>:<hst>:<nam>``) that will be varied. Note that this is changed here. :param dict constrDict: a list of dicts defining relationships/constraints (as defined in :func:`GroupConstraints`) :param list fixedList: a list of values specifying a fixed value for each dict in constrDict. Values are either strings that can be converted to floats, float values or None if the constraint defines a new parameter. :param dict parmDict: a dict containing all parameters defined in current refinement. :param int SeqHist: number of current histogram, when used in a sequential refinement. None (default) otherwise. Wildcard variable names are set to the current histogram, when found if not None. ''' global dependentParmList,arrayList,invarrayList,indParmList,consNum msg = '' # process fixed (held) variables for cdict in constrDict: if len(cdict) == 1: fixedVarList.append(cdict.keys()[0]) # process equivalences: make a list of dependent and independent vars # and check for repeated uses (repetition of a parameter as an # independent var is OK [A=B; A=C], but chaining: [A=B; B=C] is not good) indepVarList = [] depVarList = [] multdepVarList = [] translateTable = {} # lookup table for wildcard referenced variables for varlist,mapvars,multarr,invmultarr in zip( # process equivalences dependentParmList,indParmList,arrayList,invarrayList): if multarr is None: # true only if an equivalence zeromult = False for mv in mapvars: #s = '' varied = 0 notvaried = '' if mv in varyList: varied += 1 else: if notvaried: notvaried += ', ' notvaried += mv if parmDict is not None and mv not in parmDict: print "Dropping equivalence for variable "+str(mv)+". Not defined in this refinement" #msg += "\nCannot equivalence to variable "+str(mv)+". Not defined in this refinement" #continue else: if mv not in indepVarList: indepVarList.append(mv) for v,m in zip(varlist,invmultarr): if parmDict is not None and v not in parmDict: print "Dropping equivalence for variable "+str(v)+". Not defined in this refinement" continue if m == 0: zeromult = True if v in varyList: varied += 1 else: if notvaried: notvaried += ', ' notvaried += v if v in depVarList: multdepVarList.append(v) else: depVarList.append(v) if varied > 0 and varied != len(varlist)+1: msg += "\nNot all variables refined in equivalence:\n\t" s = "" for v in varlist: if s != "": s+= " & " s += str(v) msg += str(mv) + " => " + s msg += '\nNot refined: ' + notvaried + '\n' if zeromult: msg += "\nZero multiplier is invalid in equivalence:\n\t" s = "" for v in varlist: if s != "": s+= " & " s += str(v) msg += str(mv) + " => " + s + '\n' if debug: print 'indepVarList',indepVarList if debug: print 'depVarList',depVarList # check for errors: inboth = set(indepVarList).intersection(set(depVarList)) if len(inboth) > 0: msg += "\nThe following parameters(s) are used as both dependent and independent variables in Equivalence relations:\n" s = '' for var in sorted(inboth): if s != "": s+= ", " s += str(var) msg += '\t'+ s + '\n' if len(multdepVarList) > 0: msg += "\nThe following parameters(s) are used in multiple Equivalence relations as dependent variables:\n" s = '' for var in sorted(set(multdepVarList)): if s != "": s+= ", " s += str(var) msg += '\t'+ s + '\n' equivVarList = list(set(indepVarList).union(set(depVarList))) # scan through parameters in each relationship. Are all varied? If only some are # varied, create an error message. for group,varlist in zip(groups,parmlist): if len(varlist) == 1: continue for rel in group: varied = 0 notvaried = '' unused = 0 notused = '' for var in constrDict[rel]: if var.startswith('_'): continue if var.split(':')[1] == '*' and SeqHist is not None: # convert wildcard var to reference current histogram; save translation in table sv = var.split(':') sv[1] = str(SeqHist) translateTable[var] = ':'.join(sv) var = translateTable[var] if parmDict is not None and var not in parmDict: unused += 1 if notvaried: notused += ', ' notused += var if var in varyList: varied += 1 else: if notvaried: notvaried += ', ' notvaried += var if var in fixedVarList: msg += '\nError: parameter '+var+" is Fixed and used in a constraint:\n\t" msg += _FormatConstraint(constrDict[rel],fixedList[rel])+"\n" #if unused > 0:# and unused != len(VarKeys(constrDict[rel])): if unused > 0 and unused != len(VarKeys(constrDict[rel])): msg += "\nSome (but not all) variables in constraint are not defined:\n\t" msg += _FormatConstraint(constrDict[rel],fixedList[rel]) msg += '\nNot used: ' + notused + '\n' if varied > 0 and varied != len(VarKeys(constrDict[rel])): msg += "\nNot all variables refined in constraint:\n\t" msg += _FormatConstraint(constrDict[rel],fixedList[rel]) msg += '\nNot refined: ' + notvaried + '\n' # if there were errors found, go no farther if msg: print ' *** ERROR in constraint definitions! ***' print msg raise Exception # now process each group and create the relations that are needed to form # a non-singular square matrix # If all are varied and this is a constraint equation, then set VaryFree flag # so that the newly created relationships will be varied for group,varlist in zip(groups,parmlist): if len(varlist) == 1: continue # for constraints, if all included variables are refined, # set the VaryFree flag, and remaining degrees of freedom will be # varied (since consistency was checked, if any one variable is # refined, then assume that all are) varsList = [] # make a list of all the referenced variables as well VaryFree = False for rel in group: varied = 0 unused = 0 for var in VarKeys(constrDict[rel]): var = translateTable.get(var,var) # replace wildcards if parmDict is not None and var not in parmDict: unused += 1 if var not in varsList: varsList.append(var) if var in varyList: varied += 1 if fixedList[rel] is not None and varied > 0: VaryFree = True if len(varlist) < len(group): # too many relationships -- no can do msg = 'too many relationships' break # Since we checked before, if any variables are unused, then all must be. # If so, this set of relationships can be ignored if unused: if debug: print('Constraint ignored (all variables undefined)') if debug: print (' '+_FormatConstraint(constrDict[rel],fixedList[rel])) continue # fill in additional degrees of freedom try: arr = _FillArray(group,constrDict,varlist) _RowEchelon(len(group),arr,varlist) constrArr = _FillArray(group,constrDict,varlist,FillDiagonals=True) GramSchmidtOrtho(constrArr,len(group)) except: msg = 'Singular relationships' break mapvar = [] group = group[:] # scan through all generated and input relationships, we need to add to the varied list # all the new parameters where VaryFree has been set or where a New Var is varied. # # If a group does not contain any fixed values (constraint equations) # and nothing in the group is varied, drop this group, so that the # dependent parameters can be refined individually. unused = True for i in range(len(varlist)): if len(group) > 0: # get the original equation reference rel = group.pop(0) fixedval = fixedList[rel] varyflag = constrDict[rel].get('_vary',False) varname = constrDict[rel].get('_name','') else: # this relationship has been generated varyflag = False varname = '' fixedval = None if fixedval is None: # this is a new variable, not a constraint if not varname: varname = paramPrefix + str(consNum) # no assigned name, create one consNum += 1 mapvar.append(varname) # vary the new relationship if it is a degree of freedom in # a set of contraint equations or if a New Var is flagged to be varied. if VaryFree or varyflag: unused = False varyList.append(varname) # fix (prevent varying) of all the variables inside the constraint group # (dependent vars) for var in varsList: if var in varyList: varyList.remove(var) else: unused = False mapvar.append(fixedval) if unused: # skip over constraints that don't matter (w/o fixed value or any refined variables) if debug: print('Constraint ignored (all variables unrefined)') if debug: print (' '+_FormatConstraint(constrDict[rel],fixedList[rel])) continue dependentParmList.append([translateTable.get(var,var) for var in varlist]) arrayList.append(constrArr) invarrayList.append(np.linalg.inv(constrArr)) indParmList.append(mapvar) if msg: print ' *** ERROR in constraint definitions! ***' print msg print VarRemapShow(varyList) raise Exception # setup dictionary containing the fixed values global fixedDict # key is original ascii string, value is float for fixedval in fixedList: if fixedval: fixedDict[fixedval] = float(fixedval) if debug: # on debug, show what is parsed & generated, semi-readable print 50*'-' print VarRemapShow(varyList) print 'Varied: ',varyList print 'Not Varied: ',fixedVarList
[docs]def StoreEquivalence(independentVar,dependentList): '''Takes a list of dependent parameter(s) and stores their relationship to a single independent parameter (independentVar) :param str independentVar: name of master parameter that will be used to determine the value to set the dependent variables :param list dependentList: a list of parameters that will set from independentVar. Each item in the list can be a string with the parameter name or a tuple containing a name and multiplier: ``['parm1',('parm2',.5),]`` ''' global dependentParmList,arrayList,invarrayList,indParmList mapList = [] multlist = [] for var in dependentList: if isinstance(var, basestring): mult = 1.0 elif len(var) == 2: var,mult = var else: raise Exception("Cannot parse "+repr(var) + " as var or (var,multiplier)") mapList.append(var) multlist.append(tuple((mult,))) # added relationships to stored values arrayList.append(None) invarrayList.append(np.array(multlist)) indParmList.append(tuple((independentVar,))) dependentParmList.append(mapList) return
[docs]def GetDependentVars(): '''Return a list of dependent variables: e.g. variables that are constrained in terms of other variables :returns: a list of variable names ''' dependentVars = [] global dependentParmList for lst in dependentParmList: for itm in lst: dependentVars.append(itm) return dependentVars
[docs]def GetIndependentVars(): '''Return a list of independent variables: e.g. variables that are created by constraints of other variables :returns: a list of variable names ''' independentVars = [] global indParmList,fixedDict for lst in indParmList: for name in lst: if name in fixedDict: continue independentVars.append(name) return independentVars
[docs]def PrintIndependentVars(parmDict,varyList,sigDict,PrintAll=False,pFile=None): '''Print the values and uncertainties on the independent variables''' global dependentParmList,arrayList,invarrayList,indParmList,fixedDict printlist = [] mapvars = GetIndependentVars() for i,name in enumerate(mapvars): if name in fixedDict: continue if PrintAll or name in varyList: sig = sigDict.get(name) printlist.append([name,parmDict[name],sig]) if len(printlist) == 0: return s1 = '' print >>pFile,130*'-' print >>pFile,"Variables generated by constraints" printlist.append(3*[None]) for name,val,esd in printlist: if len(s1) > 120 or name is None: print >>pFile,'' print >>pFile,s1 print >>pFile,s2 print >>pFile,s3 s1 = '' if name is None: break if s1 == "": s1 = ' name :' s2 = ' value :' s3 = ' sig :' s1 += '%15s' % (name) s2 += '%15.4f' % (val) if esd is None: s3 += '%15s' % ('n/a') else: s3 += '%15.4f' % (esd)
[docs]def ComputeDepESD(covMatrix,varyList,parmDict): '''Compute uncertainties for dependent parameters from independent ones returns a dictionary containing the esd values for dependent parameters ''' sigmaDict = {} for varlist,mapvars,invmultarr in zip(dependentParmList,indParmList,invarrayList): #if invmultarr is None: continue # probably not needed try: valuelist = [parmDict[var] for var in mapvars] except KeyError: continue # get the v-covar matrix for independent parameters vcov = np.zeros((len(mapvars),len(mapvars))) for i1,name1 in enumerate(mapvars): if name1 not in varyList: continue iv1 = varyList.index(name1) for i2,name2 in enumerate(mapvars): if name2 not in varyList: continue iv2 = varyList.index(name2) vcov[i1][i2] = covMatrix[iv1][iv2] # vec is the vector that multiplies each of the independent values for v,vec in zip(varlist,invmultarr): sigmaDict[v] = np.sqrt(np.inner(vec.T,np.inner(vcov,vec))) return sigmaDict
def _FormatConstraint(RelDict,RelVal): '''Formats a Constraint or Function for use in a convenient way''' linelen = 45 s = [""] for var,val in RelDict.items(): if var.startswith('_'): continue if len(s[-1]) > linelen: s.append(' ') m = val if s[-1] != "" and m >= 0: s[-1] += ' + ' elif s[-1] != "": s[-1] += ' - ' m = abs(m) s[-1] += '%.3f*%s '%(m,var) if len(s[-1]) > linelen: s.append(' ') if RelVal is None: s[-1] += ' = New variable' else: s[-1] += ' = ' + RelVal s1 = '' for s2 in s: if s1 != '': s1 += '\n\t' s1 += s2 return s1
[docs]def VarRemapShow(varyList): '''List out the saved relationships. This should be done after the constraints have been defined using :func:`StoreEquivalence`, :func:`GroupConstraints` and :func:`GenerateConstraints`. :returns: a string containing the details of the contraint relationships ''' s = '' if len(fixedVarList) > 0: s += 'Fixed Variables:\n' for v in fixedVarList: s += ' ' + v + '\n' s += 'Variable mapping relations:\n' global dependentParmList,arrayList,invarrayList,indParmList,fixedDict for varlist,mapvars,multarr,invmultarr in zip( dependentParmList,indParmList,arrayList,invarrayList): for i,mv in enumerate(mapvars): if multarr is None: s += ' ' + str(mv) + ' is equivalent to parameter(s): ' j = 0 for v,m in zip(varlist,invmultarr): if debug: print 'v,m[0]: ',v,m[0] if j > 0: s += ' & ' j += 1 s += str(v) if m != 1: s += " / " + str(m[0]) s += '\n' continue s += ' %s = ' % mv j = 0 for m,v in zip(multarr[i,:],varlist): if m == 0: continue if j > 0: s += ' + ' j += 1 s += '(%s * %s)' % (m,v) if mv in varyList: s += ' VARY' s += '\n' s += 'Inverse variable mapping relations:\n' for varlist,mapvars,invmultarr in zip(dependentParmList,indParmList,invarrayList): for i,mv in enumerate(varlist): s += ' %s = ' % mv j = 0 for m,v in zip(invmultarr[i,:],mapvars): if m == 0: continue if j > 0: s += ' + ' j += 1 s += '(%s * %s)' % (m,v) s += '\n' return s
[docs]def Dict2Deriv(varyList,derivDict,dMdv): '''Compute derivatives for Independent Parameters from the derivatives for the original parameters :param list varyList: a list of parameters names that will be varied :param dict derivDict: a dict containing derivatives for parameter values keyed by the parameter names. :param list dMdv: a Jacobian, as a list of np.array containing derivatives for dependent parameter computed from derivDict ''' global dependentParmList,arrayList,invarrayList,indParmList,invarrayList for varlist,mapvars,multarr,invmultarr in zip(dependentParmList,indParmList,arrayList,invarrayList): for i,name in enumerate(mapvars): # grouped variables: need to add in the derv. w/r # dependent variables to the independent ones if name not in varyList: continue # skip if independent var not varied if multarr is None: for v,m in zip(varlist,invmultarr): if debug: print 'start dMdv',dMdv[varyList.index(name)] if debug: print 'add derv',v,'/',m[0],'to derv',name,'add=',derivDict[v] / m[0] if m == 0: continue dMdv[varyList.index(name)] += derivDict[v] / m[0] else: for v,m in zip(varlist,multarr[i,:]): if debug: print 'start dMdv',dMdv[varyList.index(name)] if debug: print 'add derv',v,'*',m,'to derv',name,'add=',m * derivDict[v] if m == 0: continue dMdv[varyList.index(name)] += m * derivDict[v]
[docs]def Map2Dict(parmDict,varyList): '''Create (or update) the Independent Parameters from the original set of Parameters Removes dependent variables from the varyList This should be done once, after the constraints have been defined using :func:`StoreEquivalence`, :func:`GroupConstraints` and :func:`GenerateConstraints` and before any variable refinement is done to complete the parameter dictionary by defining independent parameters and satisfying the constraint equations. :param dict parmDict: a dict containing parameter values keyed by the parameter names. This will contain updated values for both dependent and independent parameters after Dict2Map is called. It will also contain some unexpected entries of every constant value {'0':0.0} & {'1.0':1.0}, which do not cause any problems. :param list varyList: a list of parameters names that will be varied ''' # process the Independent vars: remove dependent ones from varylist # and then compute values for the Independent ones from their dependents global dependentParmList,arrayList,invarrayList,indParmList,fixedDict for varlist,mapvars,multarr in zip(dependentParmList,indParmList,arrayList): for item in varlist: try: varyList.remove(item) except ValueError: pass if multarr is None: continue valuelist = [parmDict[var] for var in varlist] parmDict.update(zip(mapvars, np.dot(multarr,np.array(valuelist))) ) # now remove fixed variables from the varyList global fixedVarList for item in fixedVarList: try: varyList.remove(item) except ValueError: pass # Set constrained parameters to their fixed values parmDict.update(fixedDict)
[docs]def Dict2Map(parmDict,varyList): '''Applies the constraints defined using :func:`StoreEquivalence`, :func:`GroupConstraints` and :func:`GenerateConstraints` by changing values in a dict containing the parameters. This should be done before the parameters are used for any computations :param dict parmDict: a dict containing parameter values keyed by the parameter names. This will contain updated values for both dependent and independent parameters after Dict2Map is called. It will also contain some unexpected entries of every constant value {'0':0.0} & {'1.0':1.0}, which do not cause any problems. :param list varyList: a list of parameters names that will be varied ''' global dependentParmList,arrayList,invarrayList,indParmList,fixedDict # reset fixed values (should not be needed, but very quick) # - this seems to update parmDict with {'0':0.0} & {'1.0':1.0} - probably not what was intended # not needed, but also not a problem - BHT parmDict.update(fixedDict) for varlist,mapvars,invmultarr in zip(dependentParmList,indParmList,invarrayList): #if invmultarr is None: continue try: valuelist = [parmDict[var] for var in mapvars] except KeyError: continue parmDict.update(zip(varlist, np.dot(invmultarr,np.array(valuelist))) ) #====================================================================== # internal routines follow (these routines are unlikely to be called # from outside the module)
[docs]def GramSchmidtOrtho(a,nkeep=0): '''Use the Gram-Schmidt process (http://en.wikipedia.org/wiki/Gram-Schmidt) to find orthonormal unit vectors relative to first row. If nkeep is non-zero, the first nkeep rows in the array are not changed input: arrayin: a 2-D non-singular square array returns: a orthonormal set of unit vectors as a square array ''' def proj(a,b): 'Projection operator' return a*(np.dot(a,b)/np.dot(a,a)) for j in range(nkeep,len(a)): for i in range(j): a[j] -= proj(a[i],a[j]) if np.allclose(np.linalg.norm(a[j]),0.0): raise Exception,"Singular input to GramSchmidtOrtho" a[j] /= np.linalg.norm(a[j]) return a
def _FillArray(sel,dict,collist,FillDiagonals=False): '''Construct a n by n matrix (n = len(collist) filling in the rows using the relationships defined in the dictionaries selected by sel If FillDiagonals is True, diagonal elements in the array are set to 1.0 ''' n = len(collist) if FillDiagonals: arr = np.eye(n) else: arr = np.zeros(2*[n,]) # fill the top rows for i,cnum in enumerate(sel): for j,var in enumerate(collist): arr[i,j] = dict[cnum].get(var,0) return arr def _SwapColumns(i,m,v): '''Swap columns in matrix m as well as the labels in v so that element (i,i) is replaced by the first non-zero element in row i after that element Throws an exception if there are no non-zero elements in that row ''' for j in range(i+1,len(v)): if not np.allclose(m[i,j],0): m[:,(i,j)] = m[:,(j,i)] v[i],v[j] = v[j],v[i] return else: raise Exception,'Singular input' def _RowEchelon(m,arr,collist): '''Convert the first m rows in Matrix arr to row-echelon form exchanging columns in the matrix and collist as needed. throws an exception if the matrix is singular because the first m rows are not linearly independent ''' n = len(collist) for i in range(m): if np.allclose(arr[i,i],0): _SwapColumns(i,arr,collist) arr[i,:] /= arr[i,i] # normalize row # subtract current row from subsequent rows to set values to left of diagonal to 0 for j in range(i+1,m): arr[j,:] -= arr[i,:] * arr[j,i] if __name__ == "__main__": parmdict = {} constrDict = [ {'0:12:Scale': 2.0, '0:11:Scale': 1.0, '0:14:Scale': 4.0, '0:13:Scale': 3.0, '0:0:Scale': 0.5}, {'0:0:eA': 0.0}, {'2::C(10,6,1)': 1.0, '1::C(10,6,1)': 1.0}, {'1::C(10,0,1)': 1.0, '2::C(10,0,1)': 1.0}, {'1::AUiso:0': 1.0, '0::AUiso:0': 1.0}, {'0::A0': 0.0} ] fixedList = ['5.0', '0', None, None, '1.0', '0'] StoreEquivalence('2::atomx:3',('2::atomy:3', ('2::atomz:3',2,), )) #StoreEquivalence('1::atomx:3',('2::atomx:3', ('2::atomz:3',2,), )) # error: dependent & independent vars mixed #StoreEquivalence('1::atomx:3',('2::atomy:3', ('2::atomz:3',2,), )) # error: dependent vars repeated #StoreEquivalence('0:1:eA',('0:0:eA',)) # error: equiv & fixed #StoreEquivalence('0:99:Scale',('0:12:Scale',)) # error: equiv & constrained #StoreEquivalence('0:12:Scale',('0:99:Scale',)) # error: equiv & constrained varylist = ['2::atomx:3', '2::C(10,6,1)', '1::C(10,6,1)', '2::atomy:3', '2::atomz:3', '0:12:Scale', '0:11:Scale', '0:14:Scale', '0:13:Scale', '0:0:Scale'] # e,w = CheckConstraints([, # [{'2:0:Scale': 1.0, '5:0:Scale': 1.0, '10:0:Scale': 1.0, '6:0:Scale': 1.0, '9:0:Scale': 1.0, '8:0:Scale': 1.0,# '3:0:Scale': 1.0, '4:0:Scale': 1.0, '7:0:Scale': 1.0, '1:0:Scale': 1.0, '0:0:Scale': 1.0}], # ['1.0']) # if e: print 'error=',e # if w: print 'error=',w # varyList = ['0::A0', '0::AUiso:0', '0::Afrac:1', '0::Afrac:2', '0::Afrac:3', '0::Afrac:4', '0::dAx:5', '0::dAy:5', '0::dAz:5', '0::AUiso:5', ':0:Back;0', ':0:Back;1', ':0:Back;2', ':0:Back;3', ':0:Back;4', ':0:Back;5', ':0:Back;6', ':0:Back;7', ':0:Back;8', ':0:Back;9', ':0:Back;10', ':0:Back;11', ':0:U', ':0:V', ':0:W', ':0:X', ':0:Y', ':0:Scale', ':0:DisplaceX', ':0:DisplaceY'] # constrDict = [ # {'0::Afrac:4': 24.0, '0::Afrac:1': 16.0, '0::Afrac:3': 24.0, '0::Afrac:2': 16.0}, # {'0::Afrac:1': 1.0, '0::Afrac:2': 1.0}, # {'0::Afrac:4': 1.0, '0::Afrac:3': 1.0}] # fixedList = ['40.0', '1.0', '1.0'] errmsg, warnmsg = CheckConstraints(varylist,constrDict,fixedList) if errmsg: print "*** Error ********************" print errmsg if warnmsg: print "*** Warning ********************" print warnmsg if errmsg or warnmsg: sys.exit() groups,parmlist = GroupConstraints(constrDict) GenerateConstraints(groups,parmlist,varylist,constrDict,fixedList) print VarRemapShow(varylist) parmdict.update( { '0:12:Scale': 1.0, '0:11:Scale': 1.0, '0:14:Scale': 1.0, '0:13:Scale': 1.0, '0:0:Scale': 2.0, '0:0:eA': 0.0, '2::C(10,6,1)': 0.2, '1::C(10,6,1)': 0.3, '1::C(10,0,1)': 0.2, '2::C(10,0,1)': 0.3, '1::AUiso:0': 0.02, '0::AUiso:0': 0.03, '0::A0': 0.0, '2::atomx:3':0.23,'2::atomy:3':-.23, '2::atomz:3':-0.11, }) print 'parmdict start',parmdict print 'varylist start',varylist before = parmdict.copy() Map2Dict(parmdict,varylist) print 'parmdict before and after Map2Dict' print ' key / before / after' for key in sorted(parmdict.keys()): print ' '+key,'\t',before.get(key),'\t',parmdict[key] print 'varylist after',varylist before = parmdict.copy() Dict2Map(parmdict,varylist) print 'after Dict2Map' print ' key / before / after' for key in sorted(parmdict.keys()): print ' '+key,'\t',before.get(key),'\t',parmdict[key] # dMdv = len(varylist)*[0] # deriv = {} # for i,v in enumerate(parmdict.keys()): deriv[v]=i # Dict2Deriv(varylist,deriv,dMdv)