Beispiel #1
0
    def copy(self, other=None):
        """copy self to other. if other is None, create new instance

        other -- reference to other object

        returns reference to copied object
        """
        # check arguments
        if other is None:
            other = FitStructure(self.name)
        elif not isinstance(other, PDFStructure):
            emsg = "other must be PDFStructure or FitStructure"
            raise ControlTypeError(emsg)
        # copy initial structure (self) to other
        PDFStructure.copy(self, other)
        # copy refined structure to other when it is FitStructure
        if isinstance(other, FitStructure):
            if self.refined is None:
                other.refined = None
            else:
                other.refined = self.refined.copy(other.refined)
        # copy constraints
        other.constraints = copy.deepcopy(self.constraints)
        other.selected_pairs = self.selected_pairs
        return other
Beispiel #2
0
    def copy(self, other=None):
        """copy self to other. if other is None, create new instance

        other -- reference to other object

        returns reference to copied object
        """
        # check arguments
        if other is None:
            other = FitStructure(self.name)
        elif not isinstance(other, PDFStructure):
            emsg = "other must be PDFStructure or FitStructure"
            raise ControlTypeError(emsg)
        # copy initial structure (self) to other
        PDFStructure.copy(self, other)
        # copy refined structure to other when it is FitStructure
        if isinstance(other, FitStructure):
            if self.refined is None:
                other.refined = None
            else:
                self.refined.copy(other.refined)
        # copy constraints
        other.constraints = copy.deepcopy(self.constraints)
        other.selected_pairs = self.selected_pairs
        return other
Beispiel #3
0
    def obtainRefined(self, server, iphase):
        """Upload refined phase from PdfFit server instance.

        server -- instance of PdfFit server
        iphase -- index of this phase in server
        """
        server.setphase(iphase)
        if self.refined is None:
            self.refined = PDFStructure(self.name)
        self.refined.readStr(server.save_struct_string(iphase), 'pdffit')
        return
Beispiel #4
0
    def __init__(self, name, *args, **kwargs):
        """Initialize FitDataSet.

        name         -- name of the data set.  The same name is used for
                        self.initial and self.final.
        args, kwargs -- optional arguments passed to parent PDFStructure
        """
        PDFStructure.__init__(self, name)
        self.owner = None
        # self.initial deliberately not assigned,
        # it gets mapped to self by __getattr__
        self.refined = None
        self.constraints = {}
        self.selected_pairs = "all-all"
        self.initial.pdffit['sgoffset'] = [0.0, 0.0, 0.0]
        self.custom_spacegroup = None
        return
Beispiel #5
0
    def __init__(self, name, *args, **kwargs):
        """Initialize FitDataSet.

        name         -- name of the data set.  The same name is used for
                        self.initial and self.final.
        args, kwargs -- optional arguments passed to parent PDFStructure
        """
        PDFStructure.__init__(self, name)
        self.owner = None
        # self.initial deliberately not assigned,
        # it gets mapped to self by __getattr__
        self.refined = None
        self.constraints = {}
        self.selected_pairs = "all-all"
        self.initial.pdffit['sgoffset'] = [0.0, 0.0, 0.0]
        self.custom_spacegroup = None
        return
Beispiel #6
0
    def readStr(self, s, format='auto'):
        """Same as PDFStructure.readStr, but handle the
        custom_spacegroup data.

        Return instance of StructureParser used to load the data.
        See Structure.readStr() for more info.
        """
        p = PDFStructure.readStr(self, s, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        return p
Beispiel #7
0
    def obtainRefined(self, server, iphase):
        """Upload refined phase from PdfFit server instance.

        server -- instance of PdfFit server
        iphase -- index of this phase in server
        """
        server.setphase(iphase)
        if self.refined is None:
            self.refined = PDFStructure(self.name)
        self.refined.readStr(server.save_struct_string(iphase), 'pdffit')
        return
Beispiel #8
0
 def test_copy(self):
     """check FitStructure.copy()
     """
     stru2 = self.stru.copy()
     self.assertEqual('noname', stru2.name)
     stru3 = Structure()
     self.assertRaises(ControlTypeError, stru2.copy, stru3)
     self.stru.refined = PDFStructure('refined-name')
     stru4 = self.stru.copy()
     self.assertIsNot(self.stru.refined, stru4.refined)
     self.assertEqual('refined-name', stru4.refined.name)
     return
Beispiel #9
0
    def load(self, z, subpath):
        """Load structure from a zipped project file.

        z       -- zipped project file
        subpath -- path to its own storage within project file
        """
        #subpath = projname/fitname/structure/myname/
        from diffpy.pdfgui.control.pdfguicontrol import CtrlUnpickler
        subs = subpath.split('/')
        rootDict = z.fileTree[subs[0]][subs[1]][subs[2]][subs[3]]
        self.initial.readStr(z.read(subpath + 'initial'), 'pdffit')
        # refined
        if rootDict.has_key('refined'):
            self.refined = PDFStructure(self.name)
            self.refined.readStr(z.read(subpath + 'refined'), 'pdffit')
        # constraints
        if rootDict.has_key('constraints'):
            self.constraints = CtrlUnpickler.loads(
                z.read(subpath + 'constraints'))
            translate = {
                'gamma': 'delta1',
                'delta': 'delta2',
                'srat': 'sratio'
            }
            for old, new in translate.items():
                if old in self.constraints:
                    self.constraints[new] = self.constraints.pop(old)
        # selected_pairs
        if rootDict.has_key("selected_pairs"):
            self.selected_pairs = z.read(subpath + 'selected_pairs')
        # sgoffset
        if rootDict.has_key("sgoffset"):
            sgoffsetstr = z.read(subpath + 'sgoffset')
            sgoffset = [float(w) for w in sgoffsetstr.split()]
            self.initial.pdffit['sgoffset'] = sgoffset
        # custom_spacegroup
        if rootDict.has_key("custom_spacegroup"):
            bytes = z.read(subpath + 'custom_spacegroup')
            self.custom_spacegroup = CtrlUnpickler.loads(bytes)
        return
Beispiel #10
0
    def readStr(self, s, format="auto"):
        """Same as PDFStructure.readStr, but handle the
        custom_spacegroup data.

        Return instance of StructureParser used to load the data.
        See Structure.readStr() for more info.
        """
        p = PDFStructure.readStr(self, s, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        # FIXME Temporary workaround to prevent forced isotropy of the Uij
        # values.  To be removed and handled by atom isotropy column.
        self.anisotropy = True
        return p
Beispiel #11
0
    def readStr(self, s, format='auto'):
        """Same as PDFStructure.readStr, but handle the
        custom_spacegroup data.

        Return instance of StructureParser used to load the data.
        See Structure.readStr() for more info.
        """
        p = PDFStructure.readStr(self, s, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        # FIXME Temporary workaround to prevent forced isotropy of the Uij
        # values.  To be removed and handled by atom isotropy column.
        self.anisotropy = True
        return p
Beispiel #12
0
    def read(self, filename, format='auto'):
        """Load structure from a file, raise ControlFileError for invalid
        or unknown structure format.  Overloads PDFStructure.read()
        to handle custom_spacegroup attribute.

        filename -- file to be loaded
        format   -- structure format such as 'pdffit', 'pdb', 'xyz'.  When
                    'auto' all available formats are tried in a row.

        Return instance of StructureParser used to load the data.
        See Structure.read() for more info.
        """
        p = PDFStructure.read(self, filename, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        return p
Beispiel #13
0
    def read(self, filename, format='auto'):
        """Load structure from a file, raise ControlFileError for invalid
        or unknown structure format.  Overloads PDFStructure.read()
        to handle custom_spacegroup attribute.

        filename -- file to be loaded
        format   -- structure format such as 'pdffit', 'pdb', 'xyz'.  When
                    'auto' all available formats are tried in a row.

        Return instance of StructureParser used to load the data.
        See Structure.read() for more info.
        """
        p = PDFStructure.read(self, filename, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        # FIXME Temporary workaround to prevent forced isotropy of the Uij
        # values.  To be removed and handled by atom isotropy column.
        self.anisotropy = True
        return p
Beispiel #14
0
    def read(self, filename, format="auto"):
        """Load structure from a file, raise ControlFileError for invalid
        or unknown structure format.  Overloads PDFStructure.read()
        to handle custom_spacegroup attribute.

        filename -- file to be loaded
        format   -- structure format such as 'pdffit', 'pdb', 'xyz'.  When
                    'auto' all available formats are tried in a row.

        Return instance of StructureParser used to load the data.
        See Structure.read() for more info.
        """
        p = PDFStructure.read(self, filename, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        # FIXME Temporary workaround to prevent forced isotropy of the Uij
        # values.  To be removed and handled by atom isotropy column.
        self.anisotropy = True
        return p
Beispiel #15
0
    def load(self, z, subpath):
        """Load structure from a zipped project file.

        z       -- zipped project file
        subpath -- path to its own storage within project file
        """
        #subpath = projname/fitname/structure/myname/
        from diffpy.pdfgui.control.pdfguicontrol import CtrlUnpickler
        subs = subpath.split('/')
        rootDict = z.fileTree[subs[0]][subs[1]][subs[2]][subs[3]]
        self.initial.readStr(z.read(subpath+'initial'), 'pdffit')
        # refined
        if rootDict.has_key('refined'):
            self.refined = PDFStructure(self.name)
            self.refined.readStr(z.read(subpath+'refined'), 'pdffit')
        # constraints
        if rootDict.has_key('constraints'):
            self.constraints = CtrlUnpickler.loads(z.read(subpath+'constraints'))
            translate = { 'gamma' : 'delta1',
                          'delta' : 'delta2',
                          'srat'  : 'sratio' }
            for old, new in translate.items():
                if old in self.constraints:
                    self.constraints[new] = self.constraints.pop(old)
        # selected_pairs
        if rootDict.has_key("selected_pairs"):
            self.selected_pairs = z.read(subpath+'selected_pairs')
        # sgoffset
        if rootDict.has_key("sgoffset"):
            sgoffsetstr = z.read(subpath+'sgoffset')
            sgoffset = [float(w) for w in sgoffsetstr.split()]
            self.initial.pdffit['sgoffset'] = sgoffset
        # custom_spacegroup
        if rootDict.has_key("custom_spacegroup"):
            bytes = z.read(subpath+'custom_spacegroup')
            self.custom_spacegroup = CtrlUnpickler.loads(bytes)
        return
Beispiel #16
0
    def load(self, z, subpath):
        """Load structure from a zipped project file.

        z       -- zipped project file
        subpath -- path to its own storage within project file
        """
        # subpath = projname/fitname/structure/myname/
        from diffpy.pdfgui.control.pdfguicontrol import CtrlUnpickler

        subs = subpath.split("/")
        rootDict = z.fileTree[subs[0]][subs[1]][subs[2]][subs[3]]
        self.initial.readStr(z.read(subpath + "initial"), "pdffit")
        # refined
        if rootDict.has_key("refined"):
            self.refined = PDFStructure(self.name)
            self.refined.readStr(z.read(subpath + "refined"), "pdffit")
        # constraints
        if rootDict.has_key("constraints"):
            self.constraints = CtrlUnpickler.loads(z.read(subpath + "constraints"))
            translate = {"gamma": "delta1", "delta": "delta2", "srat": "sratio"}
            for old, new in translate.items():
                if old in self.constraints:
                    self.constraints[new] = self.constraints.pop(old)
        # selected_pairs
        if rootDict.has_key("selected_pairs"):
            self.selected_pairs = z.read(subpath + "selected_pairs")
        # sgoffset
        if rootDict.has_key("sgoffset"):
            sgoffsetstr = z.read(subpath + "sgoffset")
            sgoffset = [float(w) for w in sgoffsetstr.split()]
            self.initial.pdffit["sgoffset"] = sgoffset
        # custom_spacegroup
        if rootDict.has_key("custom_spacegroup"):
            bytes = z.read(subpath + "custom_spacegroup")
            self.custom_spacegroup = CtrlUnpickler.loads(bytes)
        return
Beispiel #17
0
class FitStructure(PDFStructure):
    """FitStructure holds initial and refined structure and related fit
    parameters.  Inherited from PDFStructure.

    Class data members:
        symposeps   -- tolerance for recognizing site as symmetry position

    Data members (in adition to those in PDFStructure):
        owner       -- instance of parent Fitting (set in Organizer.add())
        initial     -- initial structure, same as self
        refined     -- refined structure when available or None
        constraints -- dictionary of { refvar_string : Constraint_instance }
        selected_pairs -- string of selected pairs, by default "all-all".
                       Use setSelectedPairs() and getSelectedPairs() methods
                       to access its value.
        custom_spacegroup -- instance of SpaceGroup which has no equivalent
                       in diffpy.structure.spacegroups module.  This can happen
                       after reading from a CIF file.  When equivalent space
                       group exists, custom_spacegroup is None.

    Refinable variables:  pscale, spdiameter, delta1, delta2, sratio, lat(n),
        where n=1..6,  x(i), y(i), z(i), occ(i), u11(i), u22(i), u33(i),
        u12(i), u13(i), u23(i), where i=1..Natoms

    Non-refinable variable:  rcut, stepcut
    """

    # class data members:
    symposeps = 0.001
    # evaluation of sorted_standard_space_groups deferred when necessary
    sorted_standard_space_groups = []

    def __init__(self, name, *args, **kwargs):
        """Initialize FitDataSet.

        name         -- name of the data set.  The same name is used for
                        self.initial and self.final.
        args, kwargs -- optional arguments passed to parent PDFStructure
        """
        PDFStructure.__init__(self, name)
        self.owner = None
        # self.initial deliberately not assigned,
        # it gets mapped to self by __getattr__
        self.refined = None
        self.constraints = {}
        self.selected_pairs = "all-all"
        self.initial.pdffit['sgoffset'] = [0.0, 0.0, 0.0]
        self.custom_spacegroup = None
        return


    def _update_custom_spacegroup(self, parser):
        """Helper method for read() and readStr(), which takes care
        of setting custom_spacegroup after successful reading.

        parser -- instance of StructureParser used in reading.

        No return value.
        """
        self.custom_spacegroup = None
        self.initial.pdffit['sgoffset'] = [0.0, 0.0, 0.0]
        if hasattr(parser, "spacegroup"):
            sg = parser.spacegroup
            # when sg.number is None or 0, we have a custom spacegroup
            if not sg.number:
                # overwrite sg.number with 0, an identifier for custom SG
                sg.number = 0
                self.custom_spacegroup = sg
            # here sg.number is 0 or positive integer
            self.initial.pdffit['spcgr'] = sg.short_name
        return


    def read(self, filename, format='auto'):
        """Load structure from a file, raise ControlFileError for invalid
        or unknown structure format.  Overloads PDFStructure.read()
        to handle custom_spacegroup attribute.

        filename -- file to be loaded
        format   -- structure format such as 'pdffit', 'pdb', 'xyz'.  When
                    'auto' all available formats are tried in a row.

        Return instance of StructureParser used to load the data.
        See Structure.read() for more info.
        """
        p = PDFStructure.read(self, filename, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        # FIXME Temporary workaround to prevent forced isotropy of the Uij
        # values.  To be removed and handled by atom isotropy column.
        self.anisotropy = True
        return p


    def readStr(self, s, format='auto'):
        """Same as PDFStructure.readStr, but handle the
        custom_spacegroup data.

        Return instance of StructureParser used to load the data.
        See Structure.readStr() for more info.
        """
        p = PDFStructure.readStr(self, s, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        # FIXME Temporary workaround to prevent forced isotropy of the Uij
        # values.  To be removed and handled by atom isotropy column.
        self.anisotropy = True
        return p


    def __getattr__(self, name):
        """Map self.initial to self.
        This is called only when normal attribute lookup fails.
        """
        if name == "initial":
            value = self
        else:
            emsg = "A instance has no attribute '%s'" % name
            raise AttributeError(emsg)
        return value

    def _getStrId(self):
        """make a string identifier

        return value: string id
        """
        return "p_" + self.name

    def clearRefined(self):
        """Clear all refinement results.
        """
        self.refined = None
        return

    def obtainRefined(self, server, iphase):
        """Upload refined phase from PdfFit server instance.

        server -- instance of PdfFit server
        iphase -- index of this phase in server
        """
        server.setphase(iphase)
        if self.refined is None:
            self.refined = PDFStructure(self.name)
        self.refined.readStr(server.save_struct_string(iphase), 'pdffit')
        return

    def findParameters(self):
        """Obtain dictionary of parameters used by self.constraints.
        The keys of returned dictionary are integer parameter indices, and
        the values are Parameter instances, with guessed initial values.

        returns dictionary of indices and Parameter instances
        """
        foundpars = {}
        for var, con in self.constraints.iteritems():
            con.guess(self.initial.getvar(var))
            for pidx, pguess in con.parguess.iteritems():
                # skip if already found
                if pidx in foundpars:
                    continue
                # insert to foundpars otherwise
                if pguess is not None:
                    foundpars[pidx] = Parameter(pidx, initial=pguess)
                else:
                    foundpars[pidx] = Parameter(pidx, initial=0.0)
        return foundpars

    def applyParameters(self, parameters):
        """Evaluate constraint formulas and adjust initial PDFStructure.

        parameters -- dictionary of parameter indices with Parameter
                      instance values.  Values may also be float type.
        """
        # convert values to floats
        parvalues = { }
        for pidx, par in parameters.iteritems():
            if isinstance(par, Parameter):
                parvalues[pidx] = par.initialValue()
            else:
                parvalues[pidx] = float(par)
        # evaluate constraints
        for var, con in self.constraints.iteritems():
            self.initial.setvar(var, con.evalFormula(parvalues))
        return

    def changeParameterIndex(self, oldidx, newidx):
        """Change a parameter index to a new value.

        This will replace all instances of one parameter name with another in
        this fit.
        """
        for var in self.constraints:
            formula = self.constraints[var].formula
            pat = r"@%i\b" % oldidx
            newformula = re.sub(pat, "@%i" % newidx, formula)
            self.constraints[var].formula = newformula
        return

    def _popAtomConstraints(self):
        """Take out atom-related items from the constraints dictionary.

        This is useful when atom indices are going to change due to
        insertion or removal of atoms.  See also _restoreAtomConstraints().

        Return a dictionary of atom instances vs dictionary of related
        refinable variables (stripped of "(siteindex)") and Constraint
        instances - for example {atom : {'u13' : constraint}}.
        """
        rv = {}
        # atom variable pattern
        avpat = re.compile(r'^([xyz]|occ|u11|u22|u33|u12|u13|u23)\((\d+)\)')
        for var in self.constraints.keys():
            m = avpat.match(var)
            if not m:   continue
            barevar = m.group(1)
            atomidx = int(m.group(2)) - 1
            cnts = rv.setdefault(self.initial[atomidx], {})
            cnts[barevar] = self.constraints.pop(var)
        return rv

    def _restoreAtomConstraints(self, acd):
        """Restore self.constraints from atom constraints dictionary.  This
        is useful for getting correct atom indices into refvar strings.
        See also _popAtomConstraints()

        acd -- dictionary obtained from _popAtomConstraints()
        """
        for i, a in enumerate(self.initial):
            if not a in acd: continue
            # there are some constraints for atom a
            siteindex = i + 1
            cnts = acd[a]
            for barevar, con in cnts.iteritems():
                var = barevar + "(%i)" % siteindex
                self.constraints[var] = con
        return

    def insertAtoms(self, index, atomlist):
        """Insert list of atoms before index and adjust self.constraints.

        index    -- position in the initial structure, atoms are appended
                    when larger than len(self.initial).
        atomlist -- list of atom instances.
        """
        acd = self._popAtomConstraints()
        # FIXME Temporary workaround to prevent forced isotropy of the Uij
        # values.  To be removed and handled by atom isotropy column.
        for a in atomlist:
            a.anisotropy = True
        # workaround ends here.
        self.initial[index:index] = atomlist
        self._restoreAtomConstraints(acd)
        return

    def deleteAtoms(self, indices):
        """Removed atoms at given indices and adjust self.constraints.

        indices -- list of integer indices of atoms to be deleted
        """
        acd = self._popAtomConstraints()
        # get unique, reverse sorted indices
        ruindices = dict.fromkeys(indices).keys()
        ruindices.sort()
        ruindices.reverse()
        for i in ruindices:
            self.initial.pop(i)
        self._restoreAtomConstraints(acd)
        return

    def expandSuperCell(self, mno):
        """Perform supercell expansion for this structure and adjust
        constraints for positions and lattice parameters.  New lattice
        parameters are multiplied and fractional coordinates divided by
        corresponding multiplier.  New atoms are grouped with their source
        in the original cell.

        mno -- tuple or list of three positive integer cell multipliers along
        the a, b, c axis
        """
        # check argument
        if tuple(mno) == (1, 1, 1):     return
        if min(mno) < 1:
            raise ControlValueError("mno must contain 3 positive integers")
        # back to business
        acd = self._popAtomConstraints()
        mnofloats = numpy.array(mno[:3], dtype=float)
        ijklist = [(i,j,k) for i in range(mno[0])
                    for j in range(mno[1]) for k in range(mno[2])]
        # build a list of new atoms
        newatoms = []
        for a in self.initial:
            for ijk in ijklist:
                adup = Atom(a)
                adup.xyz = (a.xyz + ijk)/mnofloats
                newatoms.append(adup)
                # does atom a have any constraint?
                if a not in acd:    continue
                # add empty constraint dictionary for duplicate atom
                acd[adup] = {}
                for barevar, con in acd[a].iteritems():
                    formula = con.formula
                    if barevar in ("x", "y", "z"):
                        symidx = "xyz".index(barevar)
                        if ijk[symidx] != 0:
                            formula += " + %i" % ijk[symidx]
                        if mno[symidx] > 1:
                            formula = "(%s)/%.1f" % (formula, mno[symidx])
                            formula = re.sub(r'\((@\d+)\)', r'\1', formula)
                    # keep other formulas intact and add constraint
                    # for barevar of the duplicate atom
                    acd[adup][barevar] = Constraint(formula)
        # replace original atoms with newatoms
        self.initial[:] = newatoms
        for ai, an in zip(self.initial, newatoms):
            if an in acd:
                acd[ai] = acd[an]
        # and rebuild their constraints
        self._restoreAtomConstraints(acd)
        # take care of lattice parameters
        self.initial.lattice.setLatPar(
                a=mno[0]*self.initial.lattice.a,
                b=mno[1]*self.initial.lattice.b,
                c=mno[2]*self.initial.lattice.c )
        # adjust lattice constraints if present
        latvars = ( "lat(1)", "lat(2)", "lat(3)" )
        for var, multiplier in zip(latvars, mno):
            if var in self.constraints and multiplier > 1:
                con = self.constraints[var]
                formula = "%.0f*(%s)" % (multiplier, con.formula)
                formula = re.sub(r'\((@\d+)\)', r'\1', formula)
                con.formula = formula
        return

    def isSpaceGroupPossible(self, spacegroup):
        """Check if space group is consistent with lattice parameters.

        spacegroup -- instance of SpaceGroup

        Return bool.
        """
        from diffpy.structure.symmetryutilities import isSpaceGroupLatPar
        return isSpaceGroupLatPar(spacegroup, *self.initial.lattice.abcABG())


    def getSpaceGroupList(self):
        """Return a list of SpaceGroup instances sorted by International
        Tables number.  When custom_spacegroup is defined, the list starts
        with custom_spacegroup.
        """
        if not FitStructure.sorted_standard_space_groups:
            import diffpy.structure.spacegroups as SG
            existing_names = {}
            unique_named_list = []
            for sg in SG.SpaceGroupList:
                if sg.short_name not in existing_names:
                    unique_named_list.append(sg)
                existing_names[sg.short_name] = True
            # sort by International Tables number, stay compatible with 2.3
            n_sg = [(sg.number % 1000, sg) for sg in unique_named_list]
            n_sg.sort()
            FitStructure.sorted_standard_space_groups = [sg for n, sg in n_sg]
        sglist = list(FitStructure.sorted_standard_space_groups)
        if self.custom_spacegroup:
            sglist.insert(0, self.custom_spacegroup)
        return sglist

    def getSpaceGroup(self, sgname):
        """Find space group in getSpaceGroupList() by short_name or number.
        sgname can be non-standard in case of CIF file defined space group.

        Return instance of SpaceGroup.
        Raise ValueError if sgname cannot be found or when it is not present
        in getSpaceGroupList().
        """
        import diffpy.structure.spacegroups as SG
        # this should match the "CIF data" sgname
        sgmatch = [sg for sg in self.getSpaceGroupList()
                if sg.short_name == sgname]
        # use standard lookup function when not matched by short_name
        if not sgmatch:
            sgmatch.append(SG.GetSpaceGroup(sgname))
        if not sgmatch:
            emsg = "Unknown space group %r" % sgname
            raise ValueError(emsg)
        sgfound = sgmatch[0]
        return sgfound

    def expandAsymmetricUnit(self, spacegroup, indices, sgoffset=[0,0,0]):
        """Perform symmetry expansion for atoms at given indices.
        Temperature factors may be corrected to reflect the symmetry.
        All constraints for expanded atoms are erased with the exception
        of the occupancy("occ".  Constraints of unaffected atoms are adjusted
        for new positions self.initial.

        spacegroup  -- instance of SpaceGroup from diffpy.structure
        indices     -- list of integer indices of atoms to be expanded
        sgoffset    -- optional offset of space group origin [0,0,0]
        """
        from diffpy.structure.symmetryutilities import ExpandAsymmetricUnit
        acd = self._popAtomConstraints()
        # get unique, reverse sorted indices
        ruindices = dict.fromkeys(indices).keys()
        ruindices.sort()
        ruindices.reverse()
        coreatoms = [self.initial[i] for i in ruindices]
        corepos = [a.xyz for a in coreatoms]
        coreUijs = [a.U for a in coreatoms]
        eau = ExpandAsymmetricUnit(spacegroup, corepos, coreUijs,
                sgoffset=sgoffset, eps=self.symposeps)
        # build a nested list of new atoms:
        newatoms = []
        for i in range(len(coreatoms)):
            ca = coreatoms[i]
            caocc_con = None
            if ca in acd and "occ" in acd[ca]:
                caocc_con = acd[ca]["occ"]
            eca = []    # expanded core atom
            for j in range(eau.multiplicity[i]):
                a = Atom(ca)
                a.xyz = eau.expandedpos[i][j]
                a.U = eau.expandedUijs[i][j]
                eca.append(a)
                if caocc_con is None:   continue
                # make a copy of occupancy constraint
                acd[a] = {"occ" : copy.copy(caocc_con)}
            newatoms.append(eca)
        # insert new atoms where they belong
        for i, atomlist in zip(ruindices, newatoms):
            self.initial[i:i+1] = atomlist
        # remember this spacegroup as the last one used
        self.initial.pdffit["spcgr"] = spacegroup.short_name
        self.initial.pdffit["sgoffset"] = list(sgoffset)
        # tidy constraints
        self._restoreAtomConstraints(acd)
        return

    def applySymmetryConstraints(self, spacegroup, indices, posflag, Uijflag,
            sgoffset=[0,0,0]):
        """Generate symmetry constraints for positions and thermal factors.
        Both positions and thermal factors may get corrected to reflect
        space group symmetry.  Old positional and thermal constraints get
        erased.  New parameter indices start at fist decade after the last
        used parameter.

        spacegroup  -- instance of SpaceGroup from diffpy.structure
        indices     -- list of integer indices of atoms to be expanded
        posflag     -- required bool flag for constraining positions
        Uijflag     -- required bool flag for Uij constrainment
        sgoffset    -- optional offset of space group origin [0,0,0]
        """
        if not posflag and not Uijflag:     return
        # need to do something
        from diffpy.structure.symmetryutilities import SymmetryConstraints
        # get unique sorted indices
        tobeconstrained = dict.fromkeys(indices)
        uindices = tobeconstrained.keys()
        uindices.sort()
        # remove old constraints
        pospat = re.compile(r'^([xyz])\((\d+)\)')
        Uijpat = re.compile(r'^(u11|u22|u33|u12|u13|u23)\((\d+)\)')
        for var in self.constraints.keys():
            mpos = posflag and pospat.match(var)
            mUij = Uijflag and Uijpat.match(var)
            if mpos and (int(mpos.group(2)) - 1) in tobeconstrained:
                del self.constraints[var]
            elif mUij and (int(mUij.group(2)) - 1) in tobeconstrained:
                del self.constraints[var]
        # find the largest used parameter index; pidxused must have an element
        pidxused = [i for i in self.owner.updateParameters()] + [0]
        # new parameters will start at the next decade
        parzeroidx = 10*(max(pidxused)/10) + 10
        # dictionary of parameter indices and their values
        newparvalues = {}
        selatoms = [self.initial[i] for i in uindices]
        selpos = [a.xyz for a in selatoms]
        selUijs = [a.U for a in selatoms]
        symcon = SymmetryConstraints(spacegroup, selpos, selUijs,
                sgoffset=sgoffset, eps=self.symposeps)
        # deal with positions
        if posflag:
            # fix positions:
            for a, xyz in zip(selatoms, symcon.positions):  a.xyz = xyz
            possymbols, parvalues = _makeParNames(symcon.pospars, parzeroidx)
            newparvalues.update(parvalues)
            eqns = symcon.positionFormulasPruned(possymbols)
            for aidx, eq in zip(uindices, eqns):
                siteidx = aidx + 1
                for barevar, formula in eq.items():
                    var = barevar + "(%i)" % siteidx
                    self.constraints[var] = Constraint(formula)
        # deal with temperature factors
        if Uijflag:
            # fix thermals
            for a, Uij in zip(selatoms, symcon.Uijs):  a.U = Uij
            Usymbols, parvalues = _makeParNames(symcon.Upars, parzeroidx)
            newparvalues.update(parvalues)
            eqns = symcon.UFormulasPruned(Usymbols)
            for aidx, eq in zip(uindices, eqns):
                siteidx = aidx + 1
                for barevar, formula in eq.items():
                    # keys in formula dictionary are uppercase
                    var = barevar.lower() + "(%i)" % siteidx
                    self.constraints[var] = Constraint(formula)
        # update parameter values in parent Fitting
        self.owner.updateParameters()
        for pidx, pvalue in newparvalues.iteritems():
            parobj = self.owner.parameters[pidx]
            parobj.setInitial(pvalue)
        # and finally remember this space group
        self.initial.pdffit["spcgr"] = spacegroup.short_name
        self.initial.pdffit["sgoffset"] = list(sgoffset)
        return

    def setSelectedPairs(self, s):
        """Set the value of selected_pairs to s, raise ControlValueError when
        s has invalid syntax.  The selected_pairs is a comma separated list of
        words formatted as

            [!]{element|indexOrRange|all}-[!]{element|indexOrRange|all}

        where '!' excludes the given atoms from first or second pair.

        Examples:

            all-all              all possible pairs
            Na-Na                only Na-Na pairs
            all-all, !Na-        all pairs except Na-Na (first index skips Na)
            all-all, -!Na        same as previous (second index skips Na)
            Na-1:4               pairs of Na and first 4 atoms
            all-all, !Cl-!Cl     exclude any pairs containing Cl
            all-all, !Cl-, -!Cl  same as previous
            1-all                only pairs including the first atom

        Use getPairSelectionFlags() method to get a list of included values
        for first and second pair index.
        """
        # check syntax of s
        psf = self.getPairSelectionFlags(s)
        self.selected_pairs = psf['fixed_pair_string']
        return

    def getSelectedPairs(self):
        return self.selected_pairs

    def getPairSelectionFlags(self, s=None):
        """Translate string s to a list of allowed values for first and second
        pair index.  Raise ControlValueError for invalid syntax of s.  See
        setSelectedPairs() docstring for a definition of pair selection syntax.

        s -- string describing selected pairs (default: self.selected_pairs)

        Return a dictionary with following keys:

        firstflags  -- list of selection flags for first indices
        secondflags -- list of selection flags for second indices
        fixed_pair_string -- argument corrected to standard syntax
        """
        if s is None:   s = self.selected_pairs
        Natoms = len(self.initial)
        # sets of first and second indices
        firstflags = Natoms * [False]
        secondflags = Natoms * [False]
        # words of fixed_pair_string
        words_fixed = []
        s1 = s.strip(' \t,')
        words = re.split(r' *, *', s1)
        for w in words:
            wparts = w.split('-')
            if len(wparts) != 2:
                emsg = "Selection word '%s' must contain one dash '-'." % w
                raise ControlValueError(emsg)
            sel0 = self._parseAtomSelectionString(wparts[0])
            sel1 = self._parseAtomSelectionString(wparts[1])
            wfixed = sel0['fixedstring'] + '-' + sel1['fixedstring']
            words_fixed.append(wfixed)
            for idx, flg in sel0['flags'].items():
                firstflags[idx] = flg
            for idx, flg in sel1['flags'].items():
                secondflags[idx] = flg
        # build returned dictionary
        rv = { 'firstflags' : firstflags,
               'secondflags' : secondflags,
               'fixed_pair_string' : ", ".join(words_fixed),
        }
        return rv

    def applyPairSelection(self, server, phaseidx):
        """Apply pair selection for calculations of partial PDF.

        server   -- instance of PdfFit engine
        phaseidx -- phase index in PdfFit engine starting from 1
        """
        psf = self.getPairSelectionFlags()
        idx = 0
        for iflag, jflag in zip(psf['firstflags'], psf['secondflags']):
            idx += 1
            server.selectAtomIndex(phaseidx, 'i', idx, iflag)
            server.selectAtomIndex(phaseidx, 'j', idx, jflag)
        return


    def getSelectedIndices(self, s):
        '''Indices of the atoms that match the specified selection string.

        s    -- selection string consisting of one or more atom selection
                words formatted as [!]{element|indexOrRange|all}
                Example: "1:4, 7, Cl".

        Return a list of integers.
        Raise ControlValueError for invalid selection string format.
        '''
        s1 = filter(lambda c: not c.isspace(), s)
        words = s1.split(',')
        indices = set()
        for w in words:
            asd = self._parseAtomSelectionString(w)
            for idx, flg in asd['flags'].items():
                if flg:
                    indices.add(idx)
                else:
                    indices.discard(idx)
        rv = sorted(indices)
        return rv


    # Regular expression object for matching atom selection strings.
    # Will be assign with the first call to _parseAtomSelectionString.
    _rxatomselection = None

    def _parseAtomSelectionString(self, s):
        '''Process string that describes a set of atoms in the structure.

        s    -- selection string formatted as [!]{element|indexOrRange|all}
                "!" negates the selection, indexOrRange can be 1, 1:4,
                where atom indices starts from 1, and "all" matches all atoms.

        Return a dictionary with following keys:
        'fixedstring'    -- selection string adjusted to standard formatting
        'flags'          -- dictionary of atom indices and boolean flags for
                            normal or negated selection.
        Raise ControlValueError for invalid string format.
        '''
        # delayed initialization of the class variable
        if self._rxatomselection is None:
            FitStructure._rxatomselection = re.compile(r'''
            (?P<negate>!?)                      # exclamation point
            (?:(?P<element>[a-zA-Z]+)$|         # element|all   or
               (?P<start>\d+)(?P<stop>:\d+)?$   # number range
            )''', re.VERBOSE)
        assert self._rxatomselection
        Natoms = len(self.initial)
        flags  = {}
        rv = {'fixedstring' : '',  'flags' : flags}
        # allow empty string and return an empty flags dictionary
        s1 = s.replace(' ', '')
        if not s1:  return rv
        mx = self._rxatomselection.match(s1)
        if not mx:
            emsg = "Invalid selection syntax in '%s'" % s
            raise ControlValueError(emsg)
        if mx.group('negate'):
            rv['fixedstring'] = '!'
        flg = not mx.group('negate')
        # process atom type
        if mx.group('element'):
            elfixed = mx.group('element')
            elfixed = elfixed[0:1].upper() + elfixed[1:].lower()
            if elfixed == 'All':
                flags.update(dict.fromkeys(range(Natoms), flg))
                rv['fixedstring'] += elfixed.lower()
            else:
                for idx in range(Natoms):
                    if self.initial[idx].element == elfixed:
                        flags[idx] = flg
                rv['fixedstring'] += elfixed
        # process range
        else:
            lo = max(int(mx.group('start')) - 1, 0)
            rv['fixedstring'] += mx.group('start')
            hi = lo + 1
            if mx.group('stop'):
                hi = int(mx.group('stop')[1:])
                rv['fixedstring'] += mx.group('stop')
            hi = min(hi, Natoms)
            flags.update(dict.fromkeys(range(lo, hi), flg))
        return rv


    def copy(self, other=None):
        """copy self to other. if other is None, create new instance

        other -- reference to other object

        returns reference to copied object
        """
        # check arguments
        if other is None:
            other = FitStructure(self.name)
        elif not isinstance(other, PDFStructure):
            emsg = "other must be PDFStructure or FitStructure"
            raise ControlTypeError(emsg)
        # copy initial structure (self) to other
        PDFStructure.copy(self, other)
        # copy refined structure to other when it is FitStructure
        if isinstance(other, FitStructure):
            if self.refined is None:
                other.refined = None
            else:
                other.refined = self.refined.copy(other.refined)
        # copy constraints
        other.constraints = copy.deepcopy(self.constraints)
        other.selected_pairs = self.selected_pairs
        return other

    def load(self, z, subpath):
        """Load structure from a zipped project file.

        z       -- zipped project file
        subpath -- path to its own storage within project file
        """
        #subpath = projname/fitname/structure/myname/
        from diffpy.pdfgui.control.pdfguicontrol import CtrlUnpickler
        subs = subpath.split('/')
        rootDict = z.fileTree[subs[0]][subs[1]][subs[2]][subs[3]]
        self.initial.readStr(z.read(subpath+'initial'), 'pdffit')
        # refined
        if rootDict.has_key('refined'):
            self.refined = PDFStructure(self.name)
            self.refined.readStr(z.read(subpath+'refined'), 'pdffit')
        # constraints
        if rootDict.has_key('constraints'):
            self.constraints = CtrlUnpickler.loads(z.read(subpath+'constraints'))
            translate = { 'gamma' : 'delta1',
                          'delta' : 'delta2',
                          'srat'  : 'sratio' }
            for old, new in translate.items():
                if old in self.constraints:
                    self.constraints[new] = self.constraints.pop(old)
        # selected_pairs
        if rootDict.has_key("selected_pairs"):
            self.selected_pairs = z.read(subpath+'selected_pairs')
        # sgoffset
        if rootDict.has_key("sgoffset"):
            sgoffsetstr = z.read(subpath+'sgoffset')
            sgoffset = [float(w) for w in sgoffsetstr.split()]
            self.initial.pdffit['sgoffset'] = sgoffset
        # custom_spacegroup
        if rootDict.has_key("custom_spacegroup"):
            bytes = z.read(subpath+'custom_spacegroup')
            self.custom_spacegroup = CtrlUnpickler.loads(bytes)
        return

    def save(self, z, subpath):
        """Save structure to a zipped project file.

        z       -- zipped project file
        subpath -- path to its own storage within project file
        """
        from diffpy.pdfgui.utils import safeCPickleDumps
        z.writestr(subpath+'initial', self.initial.writeStr('pdffit'))
        if self.refined:
            z.writestr(subpath+'refined', self.refined.writeStr('pdffit'))
        if self.constraints:
            bytes = safeCPickleDumps(self.constraints)
            z.writestr(subpath+'constraints', bytes)
        z.writestr(subpath+'selected_pairs', self.selected_pairs)
        # sgoffset
        sgoffset = self.initial.pdffit.get('sgoffset', [0.0, 0.0, 0.0])
        sgoffsetstr = "%g %g %g" % tuple(sgoffset)
        z.writestr(subpath+'sgoffset', sgoffsetstr)
        if self.custom_spacegroup:
            bytes = safeCPickleDumps(self.custom_spacegroup)
            z.writestr(subpath+'custom_spacegroup', bytes)
        return

    def getYNames(self):
        """get names of data item which can be plotted as y

        returns a name str list
        """
        return self.constraints.keys()

    def getXNames(self):
        """get names of data item which can be plotted as x

        returns a name str list
        """
        # in fact nothing
        return []

    def getData(self, name, step = -1 ):
        """get self's data member

        name -- data item name
        step -- step info, it can be:
                (1) a number ( -1 means latest step ): for single step
                (2) a list of numbers: for multiple steps
                (3) None: for all steps

        returns data object, be it a single number, a list, or a list of list
        """
        # FIXME: for next plot interface, we need find how many steps the
        # plotter is requiring for and make exact same number of copies of
        # data by name
        data = self.owner.getMetaData(name)
        if data is not None:
            return data
        return self.owner._getData(self, name, step)
class TestPDFStructure(unittest.TestCase):


    def setUp(self):
        self.stru = PDFStructure('noname')
        return


    def tearDown(self):
        return


    def test___init__(self):
        """check PDFStructure.__init__()
        """
        self.assertEqual('noname', self.stru.name)
        return


    def test_read(self):
        """check PDFStructure.read()
        """
        stru = self.stru
        notastructurefile = datafile('300K.gr')
        self.assertRaises(ControlFileError,
                stru.read, notastructurefile, format='pdffit')
        return


    def test_copy(self):
        """check PDFStructure.copy()
        """
        stru2 = self.stru.copy()
        self.assertEqual('noname', stru2.name)
        self.assertEqual(self.stru.pdffit, stru2.pdffit)
        self.assertIsNot(self.stru.pdffit['ncell'], stru2.pdffit['ncell'])
        return


    def test_setvar(self):
        """check PDFStructure.setvar()
        """
        stru = self.stru
        stru.addNewAtom('C', [0, 0, 0], anisotropy=True)
        stru.setvar('pscale', 1.5)
        self.assertEqual(1.5, stru.pdffit['scale'])
        stru.setvar('lat(1)', 4)
        stru.setvar('lat(2)', 5)
        stru.setvar('lat(3)', 7)
        self.assertEqual(4, stru.lattice.a)
        self.assertEqual(5, stru.lattice.b)
        self.assertEqual(7, stru.lattice.c)
        stru.setvar('lat(4)', 91)
        stru.setvar('lat(5)', 92)
        stru.setvar('lat(6)', 93)
        self.assertEqual(91, stru.lattice.alpha)
        self.assertEqual(92, stru.lattice.beta)
        self.assertEqual(93, stru.lattice.gamma)
        stru.setvar('spdiameter', 17)
        self.assertEqual(17, stru.pdffit['spdiameter'])
        stru.setvar('stepcut', 19)
        self.assertEqual(19, stru.pdffit['stepcut'])
        self.assertRaises(ControlKeyError,
                stru.setvar, 'sstepcut', 6)
        stru.setvar('x(1)', 0.1)
        stru.setvar('y(1)', 0.2)
        stru.setvar('z(1)', 0.3)
        stru.setvar('occ(1)', 0.9)
        stru.setvar('u23(1)', 0.004)
        self.assertEqual([0.1, 0.2, 0.3], stru[0].xyz.tolist())
        self.assertEqual(0.9, stru[0].occupancy)
        self.assertEqual(0.004, stru[0].U[1, 2])
        self.assertRaises(ControlKeyError, stru.setvar, 'invalid(1)', 7)
        return


    def test_getvar(self):
        """check PDFStructure.getvar()
        """
        from diffpy.structure import Atom
        stru = self.stru
        abcABG = (3.0, 4.0, 5.0, 81, 82, 83)
        stru.lattice.setLatPar(*abcABG)
        for i in range(6):
            self.assertEqual(abcABG[i], stru.getvar('lat(%i)' % (i + 1)))
        stru.append(Atom('Ni', [0.1, 0.2, 0.3]))
        self.assertEqual(0.1, stru.getvar('x(1)'))
        self.assertEqual(0.2, stru.getvar('y(1)'))
        self.assertEqual(0.3, stru.getvar('z(1)'))
        self.assertEqual(1.0, stru.getvar('occ(1)'))
        # pscale
        self.assertEqual(1.0, stru.getvar('pscale'))
        # spdiameter
        self.assertEqual(0.0, stru.getvar('spdiameter'))
        stru.pdffit['spdiameter'] = 37.7
        self.assertEqual(37.7, stru.getvar('spdiameter'))
        # stepcut
        self.assertEqual(0.0, stru.getvar('stepcut'))
        stru.pdffit['stepcut'] = 17.7
        self.assertEqual(17.7, stru.getvar('stepcut'))
        self.assertRaises(ControlKeyError, stru.getvar, 'invalid(1)')
        self.assertRaises(ControlKeyError, stru.getvar, 'invalid')
        return
Beispiel #19
0
class FitStructure(PDFStructure):
    """FitStructure holds initial and refined structure and related fit
    parameters.  Inherited from PDFStructure.

    Class data members:
        symposeps   -- tolerance for recognizing site as symmetry position

    Data members (in adition to those in PDFStructure):
        owner       -- instance of parent Fitting (set in Organizer.add())
        initial     -- initial structure, same as self
        refined     -- refined structure when available or None
        constraints -- dictionary of { refvar_string : Constraint_instance }
        selected_pairs -- string of selected pairs, by default "all-all".
                       Use setSelectedPairs() and getSelectedPairs() methods
                       to access its value.
        custom_spacegroup -- instance of SpaceGroup which has no equivalent
                       in diffpy.Structure.SpaceGroups module.  This can happen
                       after reading from a CIF file.  When equivalent space
                       group exists, custom_spacegroup is None.

    Refinable variables:  pscale, spdiameter, delta1, delta2, sratio, lat(n),
        where n=1..6,  x(i), y(i), z(i), occ(i), u11(i), u22(i), u33(i),
        u12(i), u13(i), u23(i), where i=1..Natoms

    Non-refinable variable:  rcut, stepcut
    """

    # class data members:
    symposeps = 0.001
    # evaluation of sorted_standard_space_groups deferred when necessary
    sorted_standard_space_groups = []

    def __init__(self, name, *args, **kwargs):
        """Initialize FitDataSet.

        name         -- name of the data set.  The same name is used for
                        self.initial and self.final.
        args, kwargs -- optional arguments passed to parent PDFStructure
        """
        PDFStructure.__init__(self, name)
        self.owner = None
        # self.initial deliberately not assigned,
        # it gets mapped to self by __getattr__
        self.refined = None
        self.constraints = {}
        self.selected_pairs = "all-all"
        self.initial.pdffit['sgoffset'] = [0.0, 0.0, 0.0]
        self.custom_spacegroup = None
        return


    def _update_custom_spacegroup(self, parser):
        """Helper method for read() and readStr(), which takes care
        of setting custom_spacegroup after successful reading.

        parser -- instance of StructureParser used in reading.

        No return value.
        """
        self.custom_spacegroup = None
        self.initial.pdffit['sgoffset'] = [0.0, 0.0, 0.0]
        if hasattr(parser, "spacegroup"):
            sg = parser.spacegroup
            # when sg.number is None or 0, we have a custom spacegroup
            if not sg.number:
                # overwrite sg.number with 0, an identifier for custom SG
                sg.number = 0
                self.custom_spacegroup = sg
            # here sg.number is 0 or positive integer
            self.initial.pdffit['spcgr'] = sg.short_name
        return


    def read(self, filename, format='auto'):
        """Load structure from a file, raise ControlFileError for invalid
        or unknown structure format.  Overloads PDFStructure.read()
        to handle custom_spacegroup attribute.

        filename -- file to be loaded
        format   -- structure format such as 'pdffit', 'pdb', 'xyz'.  When
                    'auto' all available formats are tried in a row.

        Return instance of StructureParser used to load the data.
        See Structure.read() for more info.
        """
        p = PDFStructure.read(self, filename, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        return p


    def readStr(self, s, format='auto'):
        """Same as PDFStructure.readStr, but handle the
        custom_spacegroup data.

        Return instance of StructureParser used to load the data.
        See Structure.readStr() for more info.
        """
        p = PDFStructure.readStr(self, s, format)
        # update data only after successful reading
        self._update_custom_spacegroup(p)
        return p


    def __getattr__(self, name):
        """Map self.initial to self.
        This is called only when normal attribute lookup fails.
        """
        if name == "initial":
            value = self
        else:
            emsg = "A instance has no attribute '%s'" % name
            raise AttributeError(emsg)
        return value

    def _getStrId(self):
        """make a string identifier

        return value: string id
        """
        return "p_" + self.name

    def clearRefined(self):
        """Clear all refinement results.
        """
        self.refined = None
        return

    def obtainRefined(self, server, iphase):
        """Upload refined phase from PdfFit server instance.

        server -- instance of PdfFit server
        iphase -- index of this phase in server
        """
        server.setphase(iphase)
        if self.refined is None:
            self.refined = PDFStructure(self.name)
        self.refined.readStr(server.save_struct_string(iphase), 'pdffit')
        return

    def findParameters(self):
        """Obtain dictionary of parameters used by self.constraints.
        The keys of returned dictionary are integer parameter indices, and
        the values are Parameter instances, with guessed initial values.

        returns dictionary of indices and Parameter instances
        """
        foundpars = {}
        for var, con in self.constraints.iteritems():
            con.guess(self.initial.getvar(var))
            for pidx, pguess in con.parguess.iteritems():
                # skip if already found
                if pidx in foundpars:
                    continue
                # insert to foundpars otherwise
                if pguess is not None:
                    foundpars[pidx] = Parameter(pidx, initial=pguess)
                else:
                    foundpars[pidx] = Parameter(pidx, initial=0.0)
        return foundpars

    def applyParameters(self, parameters):
        """Evaluate constraint formulas and adjust initial PDFStructure.

        parameters -- dictionary of parameter indices with Parameter
                      instance values.  Values may also be float type.
        """
        # convert values to floats
        parvalues = { }
        for pidx, par in parameters.iteritems():
            if isinstance(par, Parameter):
                parvalues[pidx] = par.initialValue()
            else:
                parvalues[pidx] = float(par)
        # evaluate constraints
        for var, con in self.constraints.iteritems():
            self.initial.setvar(var, con.evalFormula(parvalues))
        return

    def changeParameterIndex(self, oldidx, newidx):
        """Change a parameter index to a new value.

        This will replace all instances of one parameter name with another in
        this fit.
        """
        for var in self.constraints:
            formula = self.constraints[var].formula
            pat = r"@%i\b" % oldidx
            newformula = re.sub(pat, "@%i" % newidx, formula)
            self.constraints[var].formula = newformula
        return

    def _popAtomConstraints(self):
        """Take out atom-related items from the constraints dictionary.

        This is useful when atom indices are going to change due to
        insertion or removal of atoms.  See also _restoreAtomConstraints().

        Return a dictionary of atom instances vs dictionary of related
        refinable variables (stripped of "(siteindex)") and Constraint
        instances - for example {atom : {'u13' : constraint}}.
        """
        rv = {}
        # atom variable pattern
        avpat = re.compile(r'^([xyz]|occ|u11|u22|u33|u12|u13|u23)\((\d+)\)')
        for var in self.constraints.keys():
            m = avpat.match(var)
            if not m:   continue
            barevar = m.group(1)
            atomidx = int(m.group(2)) - 1
            cnts = rv.setdefault(self.initial[atomidx], {})
            cnts[barevar] = self.constraints.pop(var)
        return rv

    def _restoreAtomConstraints(self, acd):
        """Restore self.constraints from atom constraints dictionary.  This
        is useful for getting correct atom indices into refvar strings.
        See also _popAtomConstraints()

        acd -- dictionary obtained from _popAtomConstraints()
        """
        for i, a in enumerate(self.initial):
            if not a in acd: continue
            # there are some constraints for atom a
            siteindex = i + 1
            cnts = acd[a]
            for barevar, con in cnts.iteritems():
                var = barevar + "(%i)" % siteindex
                self.constraints[var] = con
        return

    def insertAtoms(self, index, atomlist):
        """Insert list of atoms before index and adjust self.constraints.

        index    -- position in the initial structure, atoms are appended
                    when larger than len(self.initial).
        atomlist -- list of atom instances.
        """
        acd = self._popAtomConstraints()
        self.initial[index:index] = atomlist
        self._restoreAtomConstraints(acd)
        return

    def deleteAtoms(self, indices):
        """Removed atoms at given indices and adjust self.constraints.

        indices -- list of integer indices of atoms to be deleted
        """
        acd = self._popAtomConstraints()
        # get unique, reverse sorted indices
        ruindices = dict.fromkeys(indices).keys()
        ruindices.sort()
        ruindices.reverse()
        for i in ruindices:
            self.initial.pop(i)
        self._restoreAtomConstraints(acd)
        return

    def expandSuperCell(self, mno):
        """Perform supercell expansion for this structure and adjust
        constraints for positions and lattice parameters.  New lattice
        parameters are multiplied and fractional coordinates divided by
        corresponding multiplier.  New atoms are grouped with their source
        in the original cell.

        mno -- tuple or list of three positive integer cell multipliers along
        the a, b, c axis
        """
        # check argument
        if tuple(mno) == (1, 1, 1):     return
        if min(mno) < 1:
            raise ControlValueError("mno must contain 3 positive integers")
        # back to business
        acd = self._popAtomConstraints()
        mnofloats = numpy.array(mno[:3], dtype=float)
        ijklist = [(i,j,k) for i in range(mno[0])
                    for j in range(mno[1]) for k in range(mno[2])]
        # build a list of new atoms
        newatoms = []
        for a in self.initial:
            for ijk in ijklist:
                adup = Atom(a)
                adup.xyz = (a.xyz + ijk)/mnofloats
                newatoms.append(adup)
                # does atom a have any constraint?
                if a not in acd:    continue
                # add empty constraint dictionary for duplicate atom
                acd[adup] = {}
                for barevar, con in acd[a].iteritems():
                    formula = con.formula
                    if barevar in ("x", "y", "z"):
                        symidx = "xyz".index(barevar)
                        if ijk[symidx] != 0:
                            formula += " + %i" % ijk[symidx]
                        if mno[symidx] > 1:
                            formula = "(%s)/%.1f" % (formula, mno[symidx])
                            formula = re.sub(r'\((@\d+)\)', r'\1', formula)
                    # keep other formulas intact and add constraint
                    # for barevar of the duplicate atom
                    acd[adup][barevar] = Constraint(formula)
        # replace original atoms with newatoms
        self.initial[:] = newatoms
        for ai, an in zip(self.initial, newatoms):
            if an in acd:
                acd[ai] = acd[an]
        # and rebuild their constraints
        self._restoreAtomConstraints(acd)
        # take care of lattice parameters
        self.initial.lattice.setLatPar(
                a=mno[0]*self.initial.lattice.a,
                b=mno[1]*self.initial.lattice.b,
                c=mno[2]*self.initial.lattice.c )
        # adjust lattice constraints if present
        latvars = ( "lat(1)", "lat(2)", "lat(3)" )
        for var, multiplier in zip(latvars, mno):
            if var in self.constraints and multiplier > 1:
                con = self.constraints[var]
                formula = "%.0f*(%s)" % (multiplier, con.formula)
                formula = re.sub(r'\((@\d+)\)', r'\1', formula)
                con.formula = formula
        return

    def isSpaceGroupPossible(self, spacegroup):
        """Check if space group is consistent with lattice parameters.

        spacegroup -- instance of SpaceGroup

        Return bool.
        """
        from diffpy.Structure.SymmetryUtilities import isSpaceGroupLatPar
        return isSpaceGroupLatPar(spacegroup, *self.initial.lattice.abcABG())


    def getSpaceGroupList(self):
        """Return a list of SpaceGroup instances sorted by International
        Tables number.  When custom_spacegroup is defined, the list starts
        with custom_spacegroup.
        """
        if not FitStructure.sorted_standard_space_groups:
            import diffpy.Structure.SpaceGroups as SG
            existing_names = {}
            unique_named_list = []
            for sg in SG.SpaceGroupList:
                if sg.short_name not in existing_names:
                    unique_named_list.append(sg)
                existing_names[sg.short_name] = True
            # sort by International Tables number, stay compatible with 2.3
            n_sg = [(sg.number % 1000, sg) for sg in unique_named_list]
            n_sg.sort()
            FitStructure.sorted_standard_space_groups = [sg for n, sg in n_sg]
        sglist = list(FitStructure.sorted_standard_space_groups)
        if self.custom_spacegroup:
            sglist.insert(0, self.custom_spacegroup)
        return sglist

    def getSpaceGroup(self, sgname):
        """Find space group in getSpaceGroupList() by short_name or number.
        sgname can be non-standard in case of CIF file defined space group.

        Return instance of SpaceGroup.
        Raise ValueError if sgname cannot be found or when it is not present
        in getSpaceGroupList().
        """
        import diffpy.Structure.SpaceGroups as SG
        # this should match the "CIF data" sgname
        sgmatch = [sg for sg in self.getSpaceGroupList()
                if sg.short_name == sgname]
        # use standard lookup function when not matched by short_name
        if not sgmatch:
            sgmatch.append(SG.GetSpaceGroup(sgname))
        if not sgmatch:
            emsg = "Unknown space group %r" % sgname
            raise ValueError(emsg)
        sgfound = sgmatch[0]
        return sgfound

    def expandAsymmetricUnit(self, spacegroup, indices, sgoffset=[0,0,0]):
        """Perform symmetry expansion for atoms at given indices.
        Temperature factors may be corrected to reflect the symmetry.
        All constraints for expanded atoms are erased with the exception
        of the occupancy("occ".  Constraints of unaffected atoms are adjusted
        for new positions self.initial.

        spacegroup  -- instance of Structure.SpaceGroup
        indices     -- list of integer indices of atoms to be expanded
        sgoffset    -- optional offset of space group origin [0,0,0]
        """
        from diffpy.Structure.SymmetryUtilities import ExpandAsymmetricUnit
        acd = self._popAtomConstraints()
        # get unique, reverse sorted indices
        ruindices = dict.fromkeys(indices).keys()
        ruindices.sort()
        ruindices.reverse()
        coreatoms = [self.initial[i] for i in ruindices]
        corepos = [a.xyz for a in coreatoms]
        coreUijs = [a.U for a in coreatoms]
        eau = ExpandAsymmetricUnit(spacegroup, corepos, coreUijs,
                sgoffset=sgoffset, eps=self.symposeps)
        # build a nested list of new atoms:
        newatoms = []
        for i in range(len(coreatoms)):
            ca = coreatoms[i]
            caocc_con = None
            if ca in acd and "occ" in acd[ca]:
                caocc_con = acd[ca]["occ"]
            eca = []    # expanded core atom
            for j in range(eau.multiplicity[i]):
                a = Atom(ca)
                a.xyz = eau.expandedpos[i][j]
                a.U = eau.expandedUijs[i][j]
                eca.append(a)
                if caocc_con is None:   continue
                # make a copy of occupancy constraint
                acd[a] = {"occ" : copy.copy(caocc_con)}
            newatoms.append(eca)
        # insert new atoms where they belong
        for i, atomlist in zip(ruindices, newatoms):
            self.initial[i:i+1] = atomlist
        # remember this spacegroup as the last one used
        self.initial.pdffit["spcgr"] = spacegroup.short_name
        self.initial.pdffit["sgoffset"] = list(sgoffset)
        # tidy constraints
        self._restoreAtomConstraints(acd)
        return

    def applySymmetryConstraints(self, spacegroup, indices, posflag, Uijflag,
            sgoffset=[0,0,0]):
        """Generate symmetry constraints for positions and thermal factors.
        Both positions and thermal factors may get corrected to reflect
        space group symmetry.  Old positional and thermal constraints get
        erased.  New parameter indices start at fist decade after the last
        used parameter.

        spacegroup  -- instance of Structure.SpaceGroup
        indices     -- list of integer indices of atoms to be expanded
        posflag     -- required bool flag for constraining positions
        Uijflag     -- required bool flag for Uij constrainment
        sgoffset    -- optional offset of space group origin [0,0,0]
        """
        if not posflag and not Uijflag:     return
        # need to do something
        from diffpy.Structure.SymmetryUtilities import SymmetryConstraints
        # get unique sorted indices
        tobeconstrained = dict.fromkeys(indices)
        uindices = tobeconstrained.keys()
        uindices.sort()
        # remove old constraints
        pospat = re.compile(r'^([xyz])\((\d+)\)')
        Uijpat = re.compile(r'^(u11|u22|u33|u12|u13|u23)\((\d+)\)')
        for var in self.constraints.keys():
            mpos = posflag and pospat.match(var)
            mUij = Uijflag and Uijpat.match(var)
            if mpos and (int(mpos.group(2)) - 1) in tobeconstrained:
                del self.constraints[var]
            elif mUij and (int(mUij.group(2)) - 1) in tobeconstrained:
                del self.constraints[var]
        # find the largest used parameter index; pidxused must have an element
        pidxused = [i for i in self.owner.updateParameters()] + [0]
        # new parameters will start at the next decade
        parzeroidx = 10*(max(pidxused)/10) + 10
        # dictionary of parameter indices and their values
        newparvalues = {}
        selatoms = [self.initial[i] for i in uindices]
        selpos = [a.xyz for a in selatoms]
        selUijs = [a.U for a in selatoms]
        symcon = SymmetryConstraints(spacegroup, selpos, selUijs,
                sgoffset=sgoffset, eps=self.symposeps)
        # deal with positions
        if posflag:
            # fix positions:
            for a, xyz in zip(selatoms, symcon.positions):  a.xyz = xyz
            possymbols, parvalues = _makeParNames(symcon.pospars, parzeroidx)
            newparvalues.update(parvalues)
            eqns = symcon.positionFormulasPruned(possymbols)
            for aidx, eq in zip(uindices, eqns):
                siteidx = aidx + 1
                for barevar, formula in eq.items():
                    var = barevar + "(%i)" % siteidx
                    self.constraints[var] = Constraint(formula)
        # deal with temperature factors
        if Uijflag:
            # fix thermals
            for a, Uij in zip(selatoms, symcon.Uijs):  a.U = Uij
            Usymbols, parvalues = _makeParNames(symcon.Upars, parzeroidx)
            newparvalues.update(parvalues)
            eqns = symcon.UFormulasPruned(Usymbols)
            for aidx, eq in zip(uindices, eqns):
                siteidx = aidx + 1
                for barevar, formula in eq.items():
                    # keys in formula dictionary are uppercase
                    var = barevar.lower() + "(%i)" % siteidx
                    self.constraints[var] = Constraint(formula)
        # update parameter values in parent Fitting
        self.owner.updateParameters()
        for pidx, pvalue in newparvalues.iteritems():
            parobj = self.owner.parameters[pidx]
            parobj.setInitial(pvalue)
        # and finally remember this space group
        self.initial.pdffit["spcgr"] = spacegroup.short_name
        self.initial.pdffit["sgoffset"] = list(sgoffset)
        return

    def setSelectedPairs(self, s):
        """Set the value of selected_pairs to s, raise ControlValueError when
        s has invalid syntax.  The selected_pairs is a comma separated list of
        words formatted as

            [!]{element|indexOrRange|all}-[!]{element|indexOrRange|all}

        where '!' excludes the given atoms from first or second pair.

        Examples:

            all-all              all possible pairs
            Na-Na                only Na-Na pairs
            all-all, !Na-        all pairs except Na-Na (first index skips Na)
            all-all, -!Na        same as previous (second index skips Na)
            Na-1:4               pairs of Na and first 4 atoms
            all-all, !Cl-!Cl     exclude any pairs containing Cl
            all-all, !Cl-, -!Cl  same as previous
            1-all                only pairs including the first atom

        Use getPairSelectionFlags() method to get a list of included values
        for first and second pair index.
        """
        # check syntax of s
        psf = self.getPairSelectionFlags(s)
        self.selected_pairs = psf['fixed_pair_string']
        return

    def getSelectedPairs(self):
        return self.selected_pairs

    def getPairSelectionFlags(self, s=None):
        """Translate string s to a list of allowed values for first and second
        pair index.  Raise ControlValueError for invalid syntax of s.  See
        setSelectedPairs() docstring for a definition of pair selection syntax.

        s -- string describing selected pairs (default: self.selected_pairs)

        Return a dictionary with following keys:

        firstflags  -- list of selection flags for first indices
        secondflags -- list of selection flags for second indices
        fixed_pair_string -- argument corrected to standard syntax
        """
        if s is None:   s = self.selected_pairs
        Natoms = len(self.initial)
        # sets of first and second indices
        firstflags = Natoms * [False]
        secondflags = Natoms * [False]
        # words of fixed_pair_string
        words_fixed = []
        s1 = s.strip(' \t,')
        words = re.split(r' *, *', s1)
        for w in words:
            wparts = w.split('-')
            if len(wparts) != 2:
                emsg = "Selection word '%s' must contain one dash '-'." % w
                raise ControlValueError(emsg)
            sel0 = self._parseAtomSelectionString(wparts[0])
            sel1 = self._parseAtomSelectionString(wparts[1])
            wfixed = sel0['fixedstring'] + '-' + sel1['fixedstring']
            words_fixed.append(wfixed)
            for idx, flg in sel0['flags'].items():
                firstflags[idx] = flg
            for idx, flg in sel1['flags'].items():
                secondflags[idx] = flg
        # build returned dictionary
        rv = { 'firstflags' : firstflags,
               'secondflags' : secondflags,
               'fixed_pair_string' : ", ".join(words_fixed),
        }
        return rv

    def applyPairSelection(self, server, phaseidx):
        """Apply pair selection for calculations of partial PDF.

        server   -- instance of PdfFit engine
        phaseidx -- phase index in PdfFit engine starting from 1
        """
        psf = self.getPairSelectionFlags()
        idx = 0
        for iflag, jflag in zip(psf['firstflags'], psf['secondflags']):
            idx += 1
            server.selectAtomIndex(phaseidx, 'i', idx, iflag)
            server.selectAtomIndex(phaseidx, 'j', idx, jflag)
        return


    def getSelectedIndices(self, s):
        '''Indices of the atoms that match the specified selection string.

        s    -- selection string consisting of one or more atom selection
                words formatted as [!]{element|indexOrRange|all}
                Example: "1:4, 7, Cl".

        Return a list of integers.
        Raise ControlValueError for invalid selection string format.
        '''
        s1 = filter(lambda c: not c.isspace(), s)
        words = s1.split(',')
        indices = set()
        for w in words:
            asd = self._parseAtomSelectionString(w)
            for idx, flg in asd['flags'].items():
                if flg:
                    indices.add(idx)
                else:
                    indices.discard(idx)
        rv = sorted(indices)
        return rv


    # Regular expression object for matching atom selection strings.
    # Will be assign with the first call to _parseAtomSelectionString.
    _rxatomselection = None

    def _parseAtomSelectionString(self, s):
        '''Process string that describes a set of atoms in the structure.

        s    -- selection string formatted as [!]{element|indexOrRange|all}
                "!" negates the selection, indexOrRange can be 1, 1:4,
                where atom indices starts from 1, and "all" matches all atoms.

        Return a dictionary with following keys:
        'fixedstring'    -- selection string adjusted to standard formatting
        'flags'          -- dictionary of atom indices and boolean flags for
                            normal or negated selection.
        Raise ControlValueError for invalid string format.
        '''
        # delayed initialization of the class variable
        if self._rxatomselection is None:
            FitStructure._rxatomselection = re.compile(r'''
            (?P<negate>!?)                      # exclamation point
            (?:(?P<element>[a-zA-Z]+)$|         # element|all   or
               (?P<start>\d+)(?P<stop>:\d+)?$   # number range
            )''', re.VERBOSE)
        assert self._rxatomselection
        Natoms = len(self.initial)
        flags  = {}
        rv = {'fixedstring' : '',  'flags' : flags}
        # allow empty string and return an empty flags dictionary
        s1 = s.replace(' ', '')
        if not s1:  return rv
        mx = self._rxatomselection.match(s1)
        if not mx:
            emsg = "Invalid selection syntax in '%s'" % s
            raise ControlValueError(emsg)
        if mx.group('negate'):
            rv['fixedstring'] = '!'
        flg = not mx.group('negate')
        # process atom type
        if mx.group('element'):
            elfixed = mx.group('element')
            elfixed = elfixed[0:1].upper() + elfixed[1:].lower()
            if elfixed == 'All':
                flags.update(dict.fromkeys(range(Natoms), flg))
                rv['fixedstring'] += elfixed.lower()
            else:
                for idx in range(Natoms):
                    if self.initial[idx].element == elfixed:
                        flags[idx] = flg
                rv['fixedstring'] += elfixed
        # process range
        else:
            lo = max(int(mx.group('start')) - 1, 0)
            rv['fixedstring'] += mx.group('start')
            hi = lo + 1
            if mx.group('stop'):
                hi = int(mx.group('stop')[1:])
                rv['fixedstring'] += mx.group('stop')
            hi = min(hi, Natoms)
            flags.update(dict.fromkeys(range(lo, hi), flg))
        return rv


    def copy(self, other=None):
        """copy self to other. if other is None, create new instance

        other -- reference to other object

        returns reference to copied object
        """
        # check arguments
        if other is None:
            other = FitStructure(self.name)
        elif not isinstance(other, PDFStructure):
            emsg = "other must be PDFStructure or FitStructure"
            raise ControlTypeError(emsg)
        # copy initial structure (self) to other
        PDFStructure.copy(self, other)
        # copy refined structure to other when it is FitStructure
        if isinstance(other, FitStructure):
            if self.refined is None:
                other.refined = None
            else:
                self.refined.copy(other.refined)
        # copy constraints
        other.constraints = copy.deepcopy(self.constraints)
        other.selected_pairs = self.selected_pairs
        return other

    def load(self, z, subpath):
        """Load structure from a zipped project file.

        z       -- zipped project file
        subpath -- path to its own storage within project file
        """
        #subpath = projname/fitname/structure/myname/
        from diffpy.pdfgui.control.pdfguicontrol import CtrlUnpickler
        subs = subpath.split('/')
        rootDict = z.fileTree[subs[0]][subs[1]][subs[2]][subs[3]]
        self.initial.readStr(z.read(subpath+'initial'), 'pdffit')
        # refined
        if rootDict.has_key('refined'):
            self.refined = PDFStructure(self.name)
            self.refined.readStr(z.read(subpath+'refined'), 'pdffit')
        # constraints
        if rootDict.has_key('constraints'):
            self.constraints = CtrlUnpickler.loads(z.read(subpath+'constraints'))
            translate = { 'gamma' : 'delta1',
                          'delta' : 'delta2',
                          'srat'  : 'sratio' }
            for old, new in translate.items():
                if old in self.constraints:
                    self.constraints[new] = self.constraints.pop(old)
        # selected_pairs
        if rootDict.has_key("selected_pairs"):
            self.selected_pairs = z.read(subpath+'selected_pairs')
        # sgoffset
        if rootDict.has_key("sgoffset"):
            sgoffsetstr = z.read(subpath+'sgoffset')
            sgoffset = [float(w) for w in sgoffsetstr.split()]
            self.initial.pdffit['sgoffset'] = sgoffset
        # custom_spacegroup
        if rootDict.has_key("custom_spacegroup"):
            bytes = z.read(subpath+'custom_spacegroup')
            self.custom_spacegroup = CtrlUnpickler.loads(bytes)
        return

    def save(self, z, subpath):
        """Save structure to a zipped project file.

        z       -- zipped project file
        subpath -- path to its own storage within project file
        """
        from diffpy.pdfgui.utils import safeCPickleDumps
        z.writestr(subpath+'initial', self.initial.writeStr('pdffit'))
        if self.refined:
            z.writestr(subpath+'refined', self.refined.writeStr('pdffit'))
        if self.constraints:
            bytes = safeCPickleDumps(self.constraints)
            z.writestr(subpath+'constraints', bytes)
        z.writestr(subpath+'selected_pairs', self.selected_pairs)
        # sgoffset
        sgoffset = self.initial.pdffit.get('sgoffset', [0.0, 0.0, 0.0])
        sgoffsetstr = "%g %g %g" % tuple(sgoffset)
        z.writestr(subpath+'sgoffset', sgoffsetstr)
        if self.custom_spacegroup:
            bytes = safeCPickleDumps(self.custom_spacegroup)
            z.writestr(subpath+'custom_spacegroup', bytes)
        return

    def getYNames(self):
        """get names of data item which can be plotted as y

        returns a name str list
        """
        return self.constraints.keys()

    def getXNames(self):
        """get names of data item which can be plotted as x

        returns a name str list
        """
        # in fact nothing
        return []

    def getData(self, name, step = -1 ):
        """get self's data member

        name -- data item name
        step -- step info, it can be:
                (1) a number ( -1 means latest step ): for single step
                (2) a list of numbers: for multiple steps
                (3) None: for all steps

        returns data object, be it a single number, a list, or a list of list
        """
        # FIXME: for next plot interface, we need find how many steps the
        # plotter is requiring for and make exact same number of copies of
        # data by name
        data = self.owner.getMetaData(name)
        if data is not None:
            return data
        return self.owner._getData(self, name, step)
 def setUp(self):
     self.stru = PDFStructure('noname')
     return