diff --git a/GSASII/GSASIIconstrGUI.py b/GSASII/GSASIIconstrGUI.py index 4408c668..70668b38 100644 --- a/GSASII/GSASIIconstrGUI.py +++ b/GSASII/GSASIIconstrGUI.py @@ -632,6 +632,8 @@ def SelectVarbs(page,FrstVarb,varList,legend,constType): l2 = max(l2,len(i2)) fmt = "{:"+str(l1)+"s} {:"+str(l2)+"s} {:s}" atchoices = [fmt.format(*i1) for i1 in choices] # reformat list as str with columns + if groupDict and '*' in str(FrstVarb) and constType == 'equivalence': + legend += '\nOK to select no additional variables' dlg = G2G.G2MultiChoiceDialog( G2frame,legend, 'Constrain '+str(FrstVarb)+' with...',atchoices, @@ -641,7 +643,7 @@ def SelectVarbs(page,FrstVarb,varList,legend,constType): Selections = dlg.GetSelections()[:] dlg.Destroy() if res != wx.ID_OK: return [] - if len(Selections) == 0: + if len(Selections) == 0 and not groupDict: dlg = wx.MessageDialog( G2frame, 'No variables were selected to include with '+str(FrstVarb), @@ -1453,7 +1455,7 @@ def UpdateConstraintPanel(panel,typ): lbl= "Define/edit constraints on refined parameters" topSizer.Add(wx.StaticText(parent,label=lbl),0,WACV) topSizer.Add((-1,-1),1,wx.EXPAND) - if G2frame.testSeqRefineMode(): + if bool(G2frame.testSeqRefineMode(True)): topSizer.Add(G2G.HelpButton(parent,helpIndex='Constraints-SeqRef')) else: topSizer.Add(G2G.HelpButton(parent,helpIndex='Constraints')) @@ -1491,7 +1493,7 @@ def UpdateConstraintPanel(panel,typ): butSizer.Add((-1,-1),1,wx.EXPAND,1) Siz.Add(butSizer,0,wx.EXPAND) - if G2frame.testSeqRefineMode(): + if bool(G2frame.testSeqRefineMode(True)): butSizer = wx.BoxSizer(wx.HORIZONTAL) butSizer.Add(wx.StaticText(panel,wx.ID_ANY,' Sequential Ref. Settings. Wildcard use: '),0,WACV) btn = G2G.EnumSelector(panel, data, '_seqmode', @@ -1499,11 +1501,14 @@ def UpdateConstraintPanel(panel,typ): ['auto-wildcard', 'wildcards-only', 'use-all'], lambda x: wx.CallAfter(UpdateConstraints, G2frame, data, G2frame.constr.GetSelection(), True)) butSizer.Add(btn,0,wx.ALIGN_CENTER_VERTICAL) - butSizer.Add(wx.StaticText(panel,wx.ID_ANY,' Selected histogram: '),0,WACV) - btn = G2G.EnumSelector(panel, data, '_seqhist', + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + if not groupDict: + butSizer.Add(wx.StaticText(panel,wx.ID_ANY,' Selected histogram: '),0,WACV) + btn = G2G.EnumSelector(panel, data, '_seqhist', list(seqHistList),list(range(len(seqHistList))), lambda x: wx.CallAfter(UpdateConstraints, G2frame, data, G2frame.constr.GetSelection(), True)) - butSizer.Add(btn,0,wx.ALIGN_CENTER_VERTICAL) + butSizer.Add(btn,0,wx.ALIGN_CENTER_VERTICAL) Siz.Add(butSizer,0) G2G.HorizontalLine(Siz,panel) # Siz.Add((5,5),0) @@ -1730,7 +1735,7 @@ def ImportConstraints(event): '_seqmode':'auto-wildcard', '_seqhist':0}) #empty dict - fill it if 'Global' not in data: #patch data['Global'] = [] - seqHistList = G2frame.testSeqRefineMode() + seqHistList = G2frame.testSeqRefineMode(True) # TODO: what about grouped Seq (groupDict)? # patch: added ~version 5030 -- new mode for wild-card use in seq refs # note that default for older sequential fits is 'wildcards-only' but in new GPX is 'auto-wildcard' if seqHistList: @@ -1752,9 +1757,12 @@ def ImportConstraints(event): #=================================================== seqmode = 'use-all' seqhistnum = None + groupDict = None if seqHistList: # Selections used with sequential refinements seqmode = data.get('_seqmode','wildcards-only') seqhistnum = min(data.get('_seqhist',0),len(seqHistList)-1) + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupDict = Controls.get('Groups',{}).get('groupDict',{}) Histograms,Phases = G2frame.GetUsedHistogramsAndPhasesfromTree() G2frame.dataWindow.ConstraintEdit.Enable(G2G.wxID_SHOWISO,True) #removed this check as it prevents examination of ISODISTORT constraints without data @@ -1795,6 +1803,11 @@ def ImportConstraints(event): ' has been run.\nWe suggest you refine a scale factor.') return + # determine if this is a grouped sequential fit + groupDict = None + if bool(G2frame.testSeqRefineMode(True)): + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupDict = Controls.get('Groups',{}).get('groupDict',{}) # create a list of the phase variables symHolds = [] (Natoms,atomIndx,phaseVary,phaseDict,pawleyLookup,FFtable,EFtable,ORBtables,BLtable,MFtable,maxSSwave) = \ @@ -1820,7 +1833,7 @@ def ImportConstraints(event): phaseAtTypes[item] = '' # create a list of the hist*phase variables - if seqHistList: # for sequential refinement, only process selected histgram in list + if seqHistList and not groupDict: # for sequential refinement, only process selected histgram in list histDict = {seqHistList[seqhistnum]:Histograms[seqHistList[seqhistnum]]} else: histDict = Histograms @@ -1964,7 +1977,7 @@ def CheckScalePhaseFractions(G2frame,hist,histograms,phases,Constraints): for histogram hist, if so, offer the user a chance to create a constraint on the sum of phase fractions ''' - if G2frame.testSeqRefineMode(): # more work needed due to seqmode + if bool(G2frame.testSeqRefineMode(True)): # more work needed due to seqmode return False # histStr = '*' else: diff --git a/GSASII/GSASIIctrlGUI.py b/GSASII/GSASIIctrlGUI.py index ffbe076e..43061a17 100644 --- a/GSASII/GSASIIctrlGUI.py +++ b/GSASII/GSASIIctrlGUI.py @@ -5080,7 +5080,7 @@ def __init__(self,G2frame,title,parmDict,varyList,fullVaryList, self.choiceDict = {} parmFrozen = Controls.get('parmFrozen',{}) - if G2frame.testSeqRefineMode(): + if bool(G2frame.testSeqRefineMode(True)): frozenList = set() for h in parmFrozen: if h == 'FrozenList': continue @@ -5229,7 +5229,7 @@ def AddSpinner(varSizer,label,SelCtrl,binding): )) parmFrozen = self.Controls.get('parmFrozen',{}) fcount = 0 - if self.G2frame.testSeqRefineMode(): + if bool(self.G2frame.testSeqRefineMode(True)): for h in parmFrozen: if h == 'FrozenList': continue fcount += len(parmFrozen[h]) @@ -5402,7 +5402,7 @@ def ResetFrozen(event): if name in self.parmWin.frozenList: del self.parmWin.frozenList[self.parmWin.frozenList.index(name)] parmFrozen = self.parmWin.Controls.get('parmFrozen',{}) - if self.parmWin.G2frame.testSeqRefineMode(): + if bool(self.parmWin.G2frame.testSeqRefineMode(True)): for h in parmFrozen: if h == 'FrozenList': continue if name in parmFrozen[h]: diff --git a/GSASII/GSASIIdataGUI.py b/GSASII/GSASIIdataGUI.py index 81bfcea3..18d6ee1a 100644 --- a/GSASII/GSASIIdataGUI.py +++ b/GSASII/GSASIIdataGUI.py @@ -68,6 +68,7 @@ def new_util_find_library( name ): from . import GSASIIfpaGUI as G2fpa from . import GSASIIseqGUI as G2seq from . import GSASIIddataGUI as G2ddG +from . import GSASIIgroupGUI as G2gr try: wx.NewIdRef @@ -964,18 +965,32 @@ def _init_Imports(self): print(f'Note: {len(msgs)} importer(s) could not be installed. See the'+ '\n "Import->Show importer error(s)" menu command for more information') - def testSeqRefineMode(self): - '''Returns the list of histograms included in a sequential refinement or - an empty list if a standard (non-sequential) refinement. - Also sets Menu item status depending on mode + def testSeqRefineMode(self, quick=False): + '''Returns the list of histograms included in a sequential + refinement or for a grouped sequential fit, a dict + where each value is a list of histograms. + + For a standard (non-sequential) refinement, an empty list + is returned. + + :param bool quick: Sets the Refine Menu item to Refine or Seq Refine + when quick=False (default setting) + :returns: an empty list for a standard refinement, a list of + histograms for a normal sequential fit or a dict for a grouped + sequential fit. ''' cId = GetGPXtreeItemId(self,self.root, 'Controls') + groupDict = None + seqSetting = None if cId: controls = self.GPXtree.GetItemPyData(cId) seqSetting = controls.get('Seq Data',[]) - else: - seqSetting = None + groupDict = controls.get('Groups',{}).get('groupDict',{}) + if groupDict: + #seqSetting = [tuple(groupDict[g]) for g in seqSetting] + seqSetting = groupDict + if quick: return seqSetting for item in self.Refine: if 'Le Bail' in item.GetItemLabel() or 'partials' in item.GetItemLabel() : item.Enable(not seqSetting) @@ -996,7 +1011,7 @@ def testSeqRefineMode(self): def PreviewFile(self,filename): 'utility to confirm we have the right file' fp = open(filename,'r') - rdmsg = u'File '+ filename +u' begins:\n\n' + rdmsg = f'File {filename} begins:\n\n' try: rdmsg += fp.read(80) rdmsg += '\n\nDo you want to read this file?' @@ -1004,7 +1019,7 @@ def PreviewFile(self,filename): rdmsg = None fp.close() if rdmsg is None or not all([ord(c) < 128 and ord(c) != 0 for c in rdmsg]): # show only if ASCII - rdmsg = u'File '+ filename +u' is a binary file. Do you want to read this file?' + rdmsg = f'File {filename} is a binary file. Do you want to read this file?' # it would be better to use something that # would resize better, but this will do for now dlg = wx.MessageDialog(self, rdmsg,'Is this the file you want?',wx.YES_NO|wx.ICON_QUESTION) @@ -1265,9 +1280,9 @@ def OnImportGeneric(self,reader,readerlist,label,multiple=False, else: if singlereader: msg += '\n'+rd.warnings - print(u'The '+ rd.formatName+u' reader was not able to read file '+filename+msg) + print(f'The {rd.formatName} reader was not able to read file {filename}{msg}') try: - print(u'\n\nError message(s):\n\t'+errorReport) + print(f'\n\nError message(s):\n\t{errorReport}') except: pass self.ErrorDialog('Read Error','The '+ rd.formatName+ @@ -1367,7 +1382,7 @@ def OnImportPhase(self,event): print('ChooseOrigin failed. Check your atom types') PhaseName = rd.Phase['General']['Name'][:] newPhaseList.append(PhaseName) - print(u'Read phase {} from file {}'.format(PhaseName,self.lastimport)) + print(f'Read phase {PhaseName} from file {self.lastimport}') sub = FindPhaseItem(self) psub = self.GPXtree.AppendItem(parent=sub,text=PhaseName) self.GPXtree.SetItemPyData(psub,rd.Phase) @@ -1624,7 +1639,7 @@ def OnImportSfact(self,event): for Bank in rd.Banks: valuesdict = {'wtFactor':1.0,'Dummy':False,'ranId':ran.randint(0,sys.maxsize),} HistName = G2obj.MakeUniqueLabel(HistName,HKLFlist) - print (u'Read structure factor table '+HistName+u' from file '+self.lastimport) + print (f'Read structure factor table {HistName} from file {self.lastimport}') Id = self.GPXtree.AppendItem(parent=self.root,text=HistName) if not Bank['RefDict'].get('FF'): Bank['RefDict']['FF'] = {} @@ -1637,7 +1652,7 @@ def OnImportSfact(self,event): else: valuesdict = {'wtFactor':1.0,'Dummy':False,'ranId':ran.randint(0,sys.maxsize),} HistName = G2obj.MakeUniqueLabel(HistName,HKLFlist) - print (u'Read structure factor table '+HistName+u' from file '+self.lastimport) + print (f'Read structure factor table {HistName} from file {self.lastimport}') if not rd.RefDict.get('FF'): rd.RefDict['FF'] = {} Id = self.GPXtree.AppendItem(parent=self.root,text=HistName) @@ -1660,6 +1675,7 @@ def OnImportSfact(self,event): header = 'Select phase(s) to add the new\nsingle crystal dataset(s) to:' for Name in newHistList: header += '\n '+str(Name) + if len(header) > 200: header = header[:200]+'...' result = G2G.ItemSelector(phaseNameList,self,header,header='Add to phase(s)',multiple=True) if not result: return # connect new phases to histograms @@ -1779,7 +1795,7 @@ def ReadPowderIparm(self,instfile,bank,databanks,rd): continue Iparm[S[:12]] = S[12:-1] except IOError: - print(u'Error reading file: {}'.format(instfile)) + print(f'Error reading file: {instfile}') if fp: fp.close() @@ -1796,9 +1812,9 @@ def ReadPowderIparm(self,instfile,bank,databanks,rd): choices.append('Bank '+str(i)) bank = 1 + G2G.BlockSelector( choices, self, - title=u'Select an instrument parameter bank for '+ - os.path.split(rd.powderentry[0])[1]+u' BANK '+str(bank)+ - u'\nOr use Cancel to select from the default parameter sets', + title='Select an instrument parameter bank for '+ + os.path.split(rd.powderentry[0])[1]+f' BANK {bank}'+ + '\nOr use Cancel to select from the default parameter sets', header='Block Selector') if bank is None: return {} # pull out requested bank # bank from the data, and change the bank to 1 @@ -1935,8 +1951,8 @@ def GetDefaultParms(self,rd): rd.instmsg = instParmList #an error message return GetDefaultParms(self,rd) else: - self.ErrorDialog('Open Error',u'Error opening instrument parameter file ' - +u'{} requested by file {}'.format(instfile,filename)) + self.ErrorDialog('Open Error','Error opening instrument parameter file ' + +f'{instfile} requested by file {filename}') #-- No other choice: ask the User for an instrument file ----------------------- while True: # loop until we get a file that works or we get a cancel instfile = '' @@ -1956,7 +1972,7 @@ def GetDefaultParms(self,rd): extOrd = [1,0] extList = ['GSAS iparm file (*.prm,*.inst,*.ins)|*.prm;*.inst;*.ins;*.PRM|','GSAS-II iparm file (*.instprm)|*.instprm|'] dlg = wx.FileDialog(self, - u'Choose inst. param file for "'+rd.idstring+u'" (or Cancel for default)', + f'Choose inst. param file for "{rd.idstring}" (or Cancel for default)', pth, '',extList[extOrd[0]]+extList[extOrd[1]]+'All files (*.*)|*.*', wx.FD_OPEN) if os.path.exists(lastIparmfile): dlg.SetFilename(os.path.split(lastIparmfile)[-1]) @@ -1981,11 +1997,11 @@ def GetDefaultParms(self,rd): if Iparm: #print 'debug: success with',instfile rd.instfile = instfile - rd.instmsg = instfile + ' bank ' + str(rd.instbank) + rd.instmsg = f'{instfile} bank {rd.instbank}' return G2fil.SetPowderInstParms(Iparm,rd) else: self.ErrorDialog('Read Error', - u'Error opening/reading file {}'.format(instfile)) + f'Error opening/reading file {instfile}') def EnableRefineCommand(self): '''Check that phases are connected to histograms - if so then @@ -2180,7 +2196,7 @@ def OnImportPowder(self,event): exec(corr) print('done') except Exception as err: - print(u'error: {}'.format(err)) + print(f'error: {err}') print('with commands -------------------') print(corr) print('---------------------------------') @@ -2245,7 +2261,7 @@ def OnImportPowder(self,event): header = 'Select phase(s) to link\nto the newly-read data:' for Name in newHistList: header += '\n '+str(Name) - + if len(header) > 200: header = header[:200]+'...' result = G2G.ItemSelector(phaseNameList,self,header,header='Add to phase(s)',multiple=True) if not result: return # connect new phases to histograms @@ -2456,8 +2472,8 @@ def OnDummyPowder(self,event): {}) self.GPXtree.Expand(Id) self.GPXtree.SelectItem(Id) - print(u'Added simulation powder data {}'.format(HistName)+ - ' with parameters from {}'.format(rd.instmsg)) + print(f'Added simulation powder data {HistName}'+ + f' with parameters from {rd.instmsg}') # make a list of phase names phaseRIdList,defPhases = self.GetPhaseInfofromTree() @@ -2597,7 +2613,7 @@ def AddSimulatedPowder(self,ttArr,intArr,HistName,Lam1,Lam2): {}) self.GPXtree.Expand(Id) self.GPXtree.SelectItem(Id) - print(u'Added simulation powder data {}'.format(HistName)) + print(f'Added simulation powder data {HistName}') return Id def OnPreferences(self,event): @@ -3325,7 +3341,7 @@ def init_vars(self): self.testRBObjSizers = {} #rigid body sizer datafile contents self.RMCchoice = 'RMCProfile' self.ifSetLimitsMode = 0 - + self.PlotBindings = [] # stores plot bindings so they can be revised def __init__(self, parent): self.ExportLookup = {} @@ -4247,15 +4263,20 @@ def OnDataDelete(self, event): item, cookie = self.GPXtree.GetFirstChild(self.root) used = False seqUse = False + seqList = self.testSeqRefineMode(True) while item: name = self.GPXtree.GetItemText(item) item, cookie = self.GPXtree.GetNextChild(self.root, cookie) if name in ['Notebook','Controls','Covariance','Constraints', - 'Restraints','Phases','Rigid bodies','Hist/Phase']: + 'Restraints','Phases','Rigid bodies','Hist/Phase', + 'Groups/Powder']: continue if 'Sequential' in name: continue - if name in self.testSeqRefineMode(): + if type(seqList) is dict and [True for i in seqList.values() if name in i]: + seqUse = True + continue + elif name in seqList: seqUse = True continue if 'PWDR' in name[:4]: @@ -4279,12 +4300,12 @@ def OnDataDelete(self, event): try: TextList.remove('PWDR'+pdfName[4:]) except ValueError: - print (u'PWDR'+pdfName[4:]+u' for '+pdfName+u' not found') + print (f'PWDR{pdfName[4:]} for {pdfName} not found') if len(TextList) == 0: if used: - msg = 'All histograms are associated with at least one phase. You must unset a histogram "use" flag in all phase(s) where it is referenced before it can be deleted' + msg = 'All histograms are associated with at least one phase. You must remove a histogram from all phase(s) where it is referenced before it can be deleted' elif seqUse: - msg = 'All histograms are in used in the sequential list. You must remove it from the list (in Controls) before it can be deleted' + msg = 'All histograms are in used in the sequential list. You must remove a histogram from the list (in Controls) before it can be deleted' else: msg = 'No data items found in tree to delete' G2G.G2MessageBox(self,msg,'Nothing to delete') @@ -4405,15 +4426,19 @@ def OnFileReopen(self, event): if len(keeplist) == 0: GSASIIpath.SetConfigValue({'previous_GPX_files':tuple([])}) return - dlg = G2G.G2SingleChoiceDialog(self,'Select previous project to open', - 'Select project',sellist) - dlg.CenterOnParent() - if dlg.ShowModal() == wx.ID_OK: - sel = dlg.GetSelection() - dlg.Destroy() + if 'GSASII-REOPEN-LAST' in [i.upper() for i in os.environ]: + sel = 0 else: - dlg.Destroy() - return + dlg = G2G.G2SingleChoiceDialog(self, + 'Select previous project to open', + 'Select project',sellist) + dlg.CenterOnParent() + if dlg.ShowModal() == wx.ID_OK: + sel = dlg.GetSelection() + dlg.Destroy() + else: + dlg.Destroy() + return filroot,dirname = sellist[sel].split(' from ') f = os.path.join(dirname,filroot) if os.path.exists(f): @@ -4423,8 +4448,8 @@ def OnFileReopen(self, event): print('file not found',f) def OnFileReread(self, event): - '''reread the current GPX file; no questions asked -- no save - for development purposes. + '''reread the current GPX file; no questions asked -- no save. + For development purposes. ''' def _setStart(nameParent,name): '''Reload the last selected tree item by name''' @@ -4456,10 +4481,10 @@ def _SaveOld(self,askSave=True): self.dataWindow.ClearData() if self.dataWindow and askSave: dlg = wx.MessageDialog(self, - 'Do you want to save and replace the current project?\n' + 'Do you want to save before replacing the current project?\n' '(Use No to read without saving or Cancel to continue ' - 'with current project)', - 'Save & Overwrite?', + 'with the current project)', + 'Save before Overwrite?', wx.YES|wx.NO|wx.CANCEL) try: result = dlg.ShowModal() @@ -4557,13 +4582,14 @@ def OnFileBrowse(self, event): print (traceback.format_exc()) - def StartProject(self): + def StartProject(self,selectItem=True): '''Opens a GSAS-II project file & selects the 1st available data set to display (PWDR, HKLF, REFD or SASD) ''' Id = 0 phaseId = None + GroupId = None seqId = None G2IO.ProjFileOpen(self) self.GPXtree.SetItemText(self.root,'Project: '+self.GSASprojectfile) @@ -4580,10 +4606,12 @@ def StartProject(self): Id = GetGPXtreeItemId(self,item,'Image Controls') else: Id = item - elif name.startswith("Sequential") and self.testSeqRefineMode(): + elif name.startswith("Sequential") and bool(self.testSeqRefineMode(True)): seqId = item elif name == "Phases": phaseId = item + elif name.startswith("Groups"): + GroupId = item elif name == 'Controls': data = self.GPXtree.GetItemPyData(item) if data: @@ -4591,19 +4619,27 @@ def StartProject(self): item, cookie = self.GPXtree.GetNextChild(self.root, cookie) if phaseId: # always show all phases self.GPXtree.Expand(phaseId) - if seqId: # open on sequential if present + if GroupId: + self.GPXtree.Expand(GroupId) + # select an item + if seqId and selectItem: # open on sequential if present self.EnablePlot = True SelectDataTreeItem(self,seqId) self.GPXtree.SelectItem(seqId) # needed on OSX or item is not selected in tree; perhaps not needed elsewhere - elif Id: # otherwise open on 1st histogram + elif GroupId and selectItem: + self.EnablePlot = True + self.GPXtree.Expand(GroupId) + SelectDataTreeItem(self,GroupId) + self.GPXtree.SelectItem(GroupId) # needed on OSX or item is not selected in tree; perhaps not needed elsewhere + elif Id and selectItem: # otherwise open on 1st histogram self.EnablePlot = True self.GPXtree.Expand(Id) SelectDataTreeItem(self,Id) self.GPXtree.SelectItem(Id) # needed on OSX or item is not selected in tree; perhaps not needed elsewhere - elif phaseId and self.GPXtree.GetChildrenCount(phaseId) > 0: # otherwise, 1st phase + elif phaseId and selectItem and self.GPXtree.GetChildrenCount(phaseId) > 0: # otherwise, 1st phase Id = phaseId # open 1st phase - Id, unused = self.GPXtree.GetFirstChild(phaseId) + Id,_ = self.GPXtree.GetFirstChild(phaseId) SelectDataTreeItem(self,Id) self.GPXtree.SelectItem(Id) # as before for OSX self.CheckNotebook() @@ -4631,8 +4667,8 @@ def OnFileClose(self, event): the project. ''' dlg = wx.MessageDialog(self, - 'Do you want to save the current project and start with an empty one?\n(Use No to clear without saving or Cancel to continue with current project)', - 'Save & Clear?', + 'Do you want to save the current project before starting with an empty one?\n(Use No to clear without saving or Cancel to continue with current project)', + 'Save before Clear?', wx.YES | wx.NO | wx.CANCEL) try: result = dlg.ShowModal() @@ -4700,10 +4736,10 @@ def SetTitleByGPX(self): ''' projName = os.path.split(self.GSASprojectfile)[1] if not projName: projName = "" - if self.testSeqRefineMode(): - s = u' (sequential refinement)' + if bool(self.testSeqRefineMode(False)): + s = ' (sequential refinement)' else: - s = u'' + s = '' self.SetTitle("GSAS-II project: "+projName + s) self.plotFrame.SetTitle("GSAS-II plots: "+projName) @@ -5444,8 +5480,7 @@ def GetUsedHistogramsAndPhasesfromTree(self): hId = histoList.index(hist) Histograms[hist]['hId'] = hId else: # would happen if a referenced histogram were renamed or deleted - print(u'For phase "'+phase+ - u'" unresolved reference to histogram "'+hist+u'"') + print(f'For phase "{phase}" unresolved reference to histogram "{hist}"') if badnum > 1: print(' ...hist not in histIdList error occured {} times'.format(badnum)) G2obj.IndexAllIds(Histograms=Histograms,Phases=phaseData) return Histograms,Phases @@ -5663,9 +5698,11 @@ def striphist(var,insChar=''): G2mv.Map2Dict(parmValDict,G2mv.saveVaryList) rigidbodyDict = self.GPXtree.GetItemPyData(GetGPXtreeItemId(self,self.root,'Rigid bodies')) - if self.testSeqRefineMode(): + if bool(self.testSeqRefineMode(True)): seqDict = self.GPXtree.GetItemPyData(GetGPXtreeItemId(self,self.root,'Sequential results')) - histNames = [h for h in self.testSeqRefineMode() if h in seqDict] + # not sure why this used the current sequential list + #histNames = [h for h in self.testSeqRefineMode(True) if h in seqDict] + histNames = [h for h in seqDict if h in Histograms] if len(histNames) == 0: print('no histograms') return @@ -5679,15 +5716,15 @@ def striphist(var,insChar=''): dlg = G2exG.ExpressionDialog(self,parmValDict, header="Evaluate an expression of GSAS-II parameters", VarLabel = "Expression", - fit=False,wildCard=self.testSeqRefineMode()) + fit=False,wildCard=bool(self.testSeqRefineMode(True))) exprobj = dlg.Show(True) if not exprobj: return - if not self.testSeqRefineMode(): + if not bool(self.testSeqRefineMode(True)): histNames = [0] for h in histNames: prfx = '' - if self.testSeqRefineMode(): + if bool(self.testSeqRefineMode(True)): parmValDict = seqDict[h]['parmDict'] covMatrix = seqDict[h]['covMatrix'] CvaryList = seqDict[h]['varyList'] @@ -5739,7 +5776,7 @@ def OnRefine(self,event): ''' Controls = self.GPXtree.GetItemPyData(GetGPXtreeItemId(self,self.root, 'Controls')) self._cleanPartials(Controls) # phase partials invalid after a refinement - if self.testSeqRefineMode(): + if bool(self.testSeqRefineMode(True)): self.OnSeqRefine(event) return @@ -5784,7 +5821,17 @@ def OnRefine(self,event): Rw = 100.00 self.SaveTreeSetting() # save the current tree selection self.GPXtree.SaveExposedItems() # save the exposed/hidden tree items - if self.PatternId and self.GPXtree.GetItemText(self.PatternId).startswith('PWDR '): + # if we are currently on a PWDR tree item or a child of one, engage "liveplot" mode + liveplot = False + if self.PickId: + for item in self.PickId,self.GPXtree.GetItemParent(self.PickId): + if self.GPXtree.GetItemText(item).startswith('PWDR'): + liveplot = True + break + if liveplot: + if GSASIIpath.GetConfigValue('debug'): print('liveplot is on') + # true when a pattern is selected for plotting, which includes + # when a group is selected. refPlotUpdate = G2pwpl.PlotPatterns(self,refineMode=True) # prepare for plot updating else: refPlotUpdate = None @@ -5883,7 +5930,8 @@ def OnRefine(self,event): ''') def OnLeBail(self,event): - '''Do a 1 cycle LeBail refinement with no other variables; usually done upon initialization of a LeBail refinement + '''Do a 0 cycle LeBail refinement where no variables are fit. + This is usually done upon initialization of a LeBail refinement either single or sequentially ''' Controls = self.GPXtree.GetItemPyData(GetGPXtreeItemId(self,self.root, 'Controls')) @@ -5895,10 +5943,11 @@ def OnLeBail(self,event): rChi2initial = 'GOF: {:.3f}'.format(covData['Rvals']['GOF']**2) except: rChi2initial = '?' - + + seqList = self.testSeqRefineMode(True) if GSASIIpath.GetConfigValue('G2RefinementWindow'): - if (self.testSeqRefineMode()): - l = len(self.testSeqRefineMode()) + if bool(seqList): + l = len(seqList) else: l = 0 dlg = G2G.G2RefinementProgress(parent=self,trialMode=False, @@ -5912,13 +5961,12 @@ def OnLeBail(self,event): else: refPlotUpdate = None - seqList = self.testSeqRefineMode() try: OK,Rvals = G2stMn.DoLeBail(self.GSASprojectfile,dlg,cycles=1,refPlotUpdate=refPlotUpdate,seqList=seqList) finally: dlg.Update(101.) # forces the Auto_Hide; needed after move w/Win & wx3.0 dlg.Destroy() - if OK and seqList: + if OK and bool(seqList): print('continuing with sequential fit') elif OK: text = '' @@ -5999,7 +6047,7 @@ def OnRefinePartials(self,event): Sets Controls['PhasePartials'] to a file name to trigger save of info in :meth:`GSASIIstrMath.getPowderProfile` and then clear that. ''' - if self.testSeqRefineMode(): # should not happen, as should not be enabled + if bool(self.testSeqRefineMode(True)): # should not happen, as should not be enabled G2G.G2MessageBox(self, 'Phase partials cannot be computed for sequential fits', 'Sequential not allowed') @@ -6237,8 +6285,9 @@ def OnSeqRefine(self,event): '''Perform a sequential refinement. Called from self.OnRefine (Which is called from the Calculate/Refine menu) ''' + allerrors = {} + allwarnings = {} Controls = self.GPXtree.GetItemPyData(GetGPXtreeItemId(self,self.root, 'Controls')) - seqList = self.testSeqRefineMode() Id = GetGPXtreeItemId(self,self.root,'Sequential results') if not Id: Id = self.GPXtree.AppendItem(self.root,text='Sequential results') @@ -6247,21 +6296,26 @@ def OnSeqRefine(self,event): Controls['ShowCell'] = True for key in ('parmMinDict','parmMaxDict','parmFrozen'): if key not in Controls: Controls[key] = {} + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + groupSeqSel = Controls['Groups'].get('groupSeqSel',list(groupDict.keys())) # check for deleted or unused histograms in refine list - phaseRIdList,histdict = self.GetPhaseInfofromTree(Used=True) - usedHistograms = [] - for k in histdict: - usedHistograms += histdict[k] - usedHistograms = list(set(usedHistograms)) - newseqList = [i for i in seqList if i in usedHistograms] - if len(newseqList) != len(seqList): - G2G.G2MessageBox(self, - str(len(seqList)-len(newseqList))+ - ' histograms that are not used have been removed from the sequential list.', - 'Histograms removed') - seqList = Controls['Seq Data'] = newseqList - allerrors = {} - allwarnings = {} + if not groupDict: + seqList = Controls.get('Seq Data',[]) + phaseRIdList,histdict = self.GetPhaseInfofromTree(Used=True) + usedHistograms = [] + for k in histdict: + usedHistograms += histdict[k] + usedHistograms = list(set(usedHistograms)) + newseqList = [i for i in seqList if i in usedHistograms] + if len(newseqList) != len(seqList): + G2G.G2MessageBox(self, + str(len(seqList)-len(newseqList))+ + ' histograms that are not used have been removed from the sequential list.', + 'Histograms removed') + seqList = Controls['Seq Data'] = newseqList + else: + seqList = groupSeqSel + Histograms,Phases = self.GetUsedHistogramsAndPhasesfromTree() # # Check if a phase lattice parameter refinement flag is set, if so transfer it to the Dij terms @@ -6298,9 +6352,18 @@ def OnSeqRefine(self,event): # save Tree to file and from here forward, work from the .gpx file not from the data tree self.OnFileSave(event) Histograms,Phases = G2stIO.GetUsedHistogramsAndPhases(self.GSASprojectfile) - for h in seqList: # check constraints are OK for each histogram to be processed - errmsg, warnmsg = G2stIO.ReadCheckConstraints(self.GSASprojectfile, - h,Histograms,Phases) + + + for i,hg in enumerate(seqList): # check constraints are OK for each histogram to be processed + # hg will be a histogram or a key to group of histograms in groupDict + if hg in groupDict: + grEquivTbl,grHIDlist = G2stIO.groupEquivTbl(hg,groupDict,Histograms,warn=True) + errmsg, warnmsg = G2stIO.ReadCheckConstraints(self.GSASprojectfile, + None,Histograms,Phases, + grHistList=groupDict[hg],grEquivTbl=grEquivTbl) + else: + errmsg, warnmsg = G2stIO.ReadCheckConstraints(self.GSASprojectfile, + hg,Histograms,Phases) if warnmsg or errmsg: print ('\nConstraint warnings/errors for histogram "{}":'.format(h)) if warnmsg: @@ -6337,12 +6400,18 @@ def OnSeqRefine(self,event): if result == wx.ID_NO: return self.GPXtree.SaveExposedItems() # find 1st histogram to be refined - if 'Seq Data' in Controls: + if groupDict: + histNames = groupSeqSel + elif 'Seq Data' in Controls: histNames = Controls['Seq Data'] else: # patch from before Controls['Seq Data'] was implemented histNames = G2stIO.GetHistogramNames(self.GSASprojectfile,['PWDR',]) if Controls.get('Reverse Seq'): histNames.reverse() + if histNames[0] in groupDict: + firstHist = groupDict[histNames[0]] + else: + firstHist = histNames[0] if Controls.get('newLeBail',False): dlgtxt = '''Do Le Bail refinement of intensities first? @@ -6357,14 +6426,14 @@ def OnSeqRefine(self,event): if result == wx.ID_YES: res = self.OnLeBail(event) if res: return - # select it + # select 1st histogram in tree if GSASIIpath.GetConfigValue('G2RefinementWindow'): trialMode = 'analytic Hessian' in Controls['deriv type'] dlgp = G2G.G2RefinementProgress(parent=self,trialMode=trialMode, seqLen=len(histNames)) else: dlgp = G2G.RefinementProgress('Residual for histogram 0','Powder profile Rwp =',parent=self) - self.PatternId = GetGPXtreeItemId(self,self.root,histNames[0]) + self.PatternId = GetGPXtreeItemId(self,self.root,firstHist) if self.PatternId and self.GPXtree.GetItemText(self.PatternId).startswith('PWDR '): refPlotUpdate = G2pwpl.PlotPatterns(self,refineMode=True) # prepare for plot updating else: @@ -7821,6 +7890,29 @@ def _makemenu(): # routine to create menu when first used # don't know which menu was selected, but should be General on first phase use SetDataMenuBar(G2frame,self.DataGeneral) self.DataGeneral = _makemenu + + # Groups + G2G.Define_wxId('wxID_GRPALL','wxID_GRPSEL') + G2G.Define_wxId('wxID_GRPNONE','wxID_GRPLOG','wxID_GRPLIN') + #G2G.Define_wxId('wxID_HIDESAME') + def _makemenu(): # routine to create menu when first used + self.GroupMenu = wx.MenuBar() + self.PrefillDataMenu(self.GroupMenu) + self.GroupCmd = wx.Menu(title='') + self.GroupMenu.Append(menu=self.GroupCmd, title='Grp Cmds') + self.GroupCmd.Append(G2G.wxID_GRPALL,'Copy all','Copy all parameters by group') + self.GroupCmd.Append(G2G.wxID_GRPSEL,'Copy selected','Copy elected parameters by group') + + self.GroupCmd.Append(G2G.wxID_GRPNONE,'No shift coloring', + 'Entry boxes are not colored', wx.ITEM_RADIO) + self.GroupCmd.Append(G2G.wxID_GRPLOG,'Log shift colors', + 'Entry boxes are colored with log steps', wx.ITEM_RADIO) + self.GroupCmd.Append(G2G.wxID_GRPLIN,'Linear shift colors', + 'Entry boxes are colored with linear steps', wx.ITEM_RADIO) +# self.GroupCmd.Append(G2G.wxID_HIDESAME,'Hide identical rows','Omit rows that are the same and are not refinable from table') + self.PostfillDataMenu() + SetDataMenuBar(G2frame,self.GroupMenu) + self.GroupMenu = _makemenu # end of GSAS-II menu definitions def readFromFile(reader): @@ -8152,6 +8244,33 @@ def OnSelectData(event): wx.CallAfter(UpdateControls,G2frame,data) + + def OnSelectGroups(event): + choices = list(groupDict.keys()) + if len(choices) == 0: + G2G.G2MessageBox(G2frame,'No groups found for a sequential fit.','No Groups') + return + while True: + sel = [] + if 'Seq Data' in data: + sel = [choices.index(item) for item in groupSeqSel if item in groupDict] + dlg = G2G.G2MultiChoiceDialog(G2frame, + 'Select groups to include. You must have at least one group selected, or use "Clear groupings"', + 'Sequential refinement selection',choices) + dlg.SetSelections(sel) + names = [] + if dlg.ShowModal() == wx.ID_OK: + for sel in dlg.GetSelections(): + names.append(choices[sel]) + dlg.Destroy() + else: + dlg.Destroy() + return + if len(names) == 0: continue # nothing selected, try again + data['Groups']['groupSeqSel'] = names + wx.CallAfter(UpdateControls,G2frame,data) + break + def OnReverse(event): data['Reverse Seq'] = reverseSel.GetValue() @@ -8171,17 +8290,66 @@ def OnClrSeq(event): seqSizer = wx.BoxSizer(wx.VERTICAL) dataSizer = wx.BoxSizer(wx.HORIZONTAL) SeqData = data.get('Seq Data',[]) - if not SeqData: - dataSizer.Add(wx.StaticText(G2frame.dataWindow,label=' Select datasets to switch to sequential refinement: '),0,WACV) - selSeqData = wx.Button(G2frame.dataWindow,label='Select datasets') - else: - lbl = 'Sequential Refinement with '+str(len(SeqData))+' dataset(s) selected' + data['Groups'] = data.get('Groups',{}) + groupDict = data['Groups']['groupDict'] = data['Groups'].get('groupDict',{}) + groupSeqSel = data['Groups']['groupSeqSel'] = data['Groups'].get('groupSeqSel',[]) + if len(groupSeqSel) == 0: # can't have groups w/o a sequential fit + groupSeqSel = data['Groups']['groupSeqSel'] = list(groupDict.keys()) + + if groupDict: + dataSizer = wx.BoxSizer(wx.HORIZONTAL) + dataSizer.Add(wx.StaticText(G2frame.dataWindow,label='Histogram Grouping: '),0,WACV) + groupCount = [len(groupDict[k]) for k in groupDict] + if min(groupCount) == max(groupCount): + msg = f'Have {len(groupDict)} group(s) with {min(groupCount)} histograms in each' + else: + msg = (f'Have {len(groupDict)} group(s) with {min(groupCount)}' + f' to {min(groupCount)} histograms in each') + notGrouped = data.get('Groups',{}).get('notGrouped',0) + if notGrouped: + msg += f". {notGrouped} not in a group" + dataSizer.Add(wx.StaticText(G2frame.dataWindow,label=msg),0,WACV) + dataSizer.Add((5,-1)) + btn = wx.Button(G2frame.dataWindow, wx.ID_ANY,'Redefine groupings') + btn.Bind(wx.EVT_BUTTON,SearchGroups) + dataSizer.Add((5,-1)) + dataSizer.Add(btn) + btn = wx.Button(G2frame.dataWindow, wx.ID_ANY,'Clear groupings') + btn.Bind(wx.EVT_BUTTON,ClearGroups) + dataSizer.Add((5,-1)) + dataSizer.Add(btn) + seqSizer.Add(dataSizer) + dataSizer = wx.BoxSizer(wx.HORIZONTAL) + lbl = f'Sequential Refinement with {len(groupSeqSel)} groups(s) selected' + dataSizer.Add(wx.StaticText(G2frame.dataWindow,label=lbl),0,WACV) + btn = wx.Button(G2frame.dataWindow, wx.ID_ANY,'Select groups in seq. fit') + btn.Bind(wx.EVT_BUTTON,OnSelectGroups) + dataSizer.Add((5,-1)) + dataSizer.Add(btn) + seqSizer.Add(dataSizer) + elif SeqData: + dataSizer = wx.BoxSizer(wx.HORIZONTAL) + lbl = f'Sequential Refinement with {len(SeqData)} dataset(s) selected' dataSizer.Add(wx.StaticText(G2frame.dataWindow,label=lbl),0,WACV) selSeqData = wx.Button(G2frame.dataWindow,label=' Reselect datasets') - selSeqData.Bind(wx.EVT_BUTTON,OnSelectData) - dataSizer.Add(selSeqData,0,WACV) - seqSizer.Add(dataSizer) - if SeqData: + selSeqData.Bind(wx.EVT_BUTTON,OnSelectData) + dataSizer.Add(selSeqData) + seqSizer.Add(dataSizer) + else: + dataSizer = wx.BoxSizer(wx.HORIZONTAL) + dataSizer.Add(wx.StaticText(G2frame.dataWindow,label=' Select datasets to switch to sequential refinement: '),0,WACV) + selSeqData = wx.Button(G2frame.dataWindow,label='Select datasets') + selSeqData.Bind(wx.EVT_BUTTON,OnSelectData) + dataSizer.Add(selSeqData,0,WACV) + seqSizer.Add(dataSizer) + dataSizer = wx.BoxSizer(wx.HORIZONTAL) + dataSizer.Add(wx.StaticText(G2frame.dataWindow,label='Histogram Grouping: '),0,WACV) + btn = wx.Button(G2frame.dataWindow, wx.ID_ANY,'Define groupings') + btn.Bind(wx.EVT_BUTTON,SearchGroups) + dataSizer.Add(btn) + seqSizer.Add(dataSizer) + + if SeqData or groupDict: selSizer = wx.BoxSizer(wx.HORIZONTAL) reverseSel = wx.CheckBox(G2frame.dataWindow,-1,label=' Reverse order?') reverseSel.Bind(wx.EVT_CHECKBOX,OnReverse) @@ -8191,7 +8359,7 @@ def OnClrSeq(event): copySel.Bind(wx.EVT_CHECKBOX,OnCopySel) copySel.SetValue(data['Copy2Next']) selSizer.Add(copySel,0,WACV) - clrSeq = wx.Button(G2frame.dataWindow,label='Clear previous seq. results') + clrSeq = wx.Button(G2frame.dataWindow,label='Clear previous seq. results table') clrSeq.Bind(wx.EVT_BUTTON,OnClrSeq) selSizer.Add(clrSeq,0,WACV) seqSizer.Add(selSizer,0) @@ -8281,26 +8449,63 @@ def OnFsqRef(event): ShklSizer.Add(usrrej,0,WACV) return LSSizer,ShklSizer - def AuthSizer(): - def OnAuthor(event): - event.Skip() - data['Author'] = auth.GetValue() - - Author = data['Author'] - authSizer = wx.BoxSizer(wx.HORIZONTAL) - authSizer.Add(wx.StaticText(G2frame.dataWindow,label=' CIF Author (last, first):'),0,WACV) - auth = wx.TextCtrl(G2frame.dataWindow,-1,value=Author,style=wx.TE_PROCESS_ENTER) - auth.Bind(wx.EVT_TEXT_ENTER,OnAuthor) - auth.Bind(wx.EVT_KILL_FOCUS,OnAuthor) - authSizer.Add(auth,0,WACV) - return authSizer + # def AuthSizer(): + # def OnAuthor(event): + # event.Skip() + # data['Author'] = auth.GetValue() + # + # Author = data['Author'] + # authSizer = wx.BoxSizer(wx.HORIZONTAL) + # authSizer.Add(wx.StaticText(G2frame.dataWindow,label=' CIF Author (last, first):'),0,WACV) + # auth = wx.TextCtrl(G2frame.dataWindow,-1,value=Author,style=wx.TE_PROCESS_ENTER) + # auth.Bind(wx.EVT_TEXT_ENTER,OnAuthor) + # auth.Bind(wx.EVT_KILL_FOCUS,OnAuthor) + # authSizer.Add(auth,0,WACV) + # return authSizer def ClearFrozen(event): 'Removes all frozen parameters by clearing the entire dict' Controls['parmFrozen'] = {} wx.CallAfter(UpdateControls,G2frame,data) - # start of UpdateControls + def SearchGroups(event): + '''Create a dict to group similar histograms. Similarity + is judged by a common string that matches a template + supplied by the user + ''' + Histograms,Phases = G2frame.GetUsedHistogramsAndPhasesfromTree() + for hist in Histograms: + if hist.startswith('PWDR '): + break + else: + G2G.G2MessageBox(G2frame,'No used PWDR histograms found to group. Histograms must be assigned phase(s).', + 'Cannot group') + return + ans = G2frame.OnFileSave(None) + if not ans: return + data['Groups'] = G2gr.SearchGroups(G2frame,Histograms,hist) +# wx.CallAfter(UpdateControls,G2frame,data) + ans = G2frame.OnFileSave(None) + if not ans: return + G2frame.clearProject() # clear out data tree + G2frame.StartProject(False) + #self.EnablePlot = True + Id = GetGPXtreeItemId(G2frame,G2frame.root, 'Controls') + SelectDataTreeItem(G2frame,Id) + G2frame.GPXtree.SelectItem(Id) # needed on OSX or item is not selected in tree; perhaps not needed elsewhere + + def ClearGroups(event): + del data['Groups'] + ans = G2frame.OnFileSave(None) + if not ans: return + G2frame.clearProject() # clear out data tree + G2frame.StartProject(False) + #self.EnablePlot = True + Id = GetGPXtreeItemId(G2frame,G2frame.root, 'Controls') + SelectDataTreeItem(G2frame,Id) + G2frame.GPXtree.SelectItem(Id) # needed on OSX or item is not selected in tree; perhaps not needed elsewhere + + #======= start of UpdateControls =========================================== if 'SVD' in data['deriv type']: G2frame.GetStatusBar().SetStatusText('Hessian SVD not recommended for initial refinements; use analytic Hessian or Jacobian',1) else: @@ -8339,19 +8544,16 @@ def ClearFrozen(event): mainSizer.Add(SeqSizer()) mainSizer.Add((5,15),0) G2G.HorizontalLine(mainSizer,G2frame.dataWindow) - subSizer = wx.BoxSizer(wx.HORIZONTAL) - subSizer.Add((-1,-1),1,wx.EXPAND) - subSizer.Add(wx.StaticText(G2frame.dataWindow,label='Global Settings'),0,WACV) - subSizer.Add((-1,-1),1,wx.EXPAND) - mainSizer.Add(subSizer,0,wx.EXPAND) - mainSizer.Add(AuthSizer()) - mainSizer.Add((5,5),0) + # subSizer.Add(wx.StaticText(G2frame.dataWindow,label='Global Settings'),0,WACV) + # subSizer.Add((-1,-1),1,wx.EXPAND) + # mainSizer.Add(subSizer,0,wx.EXPAND) + # mainSizer.Add(AuthSizer()) Controls = data # count frozen variables (in appropriate place) for key in ('parmMinDict','parmMaxDict','parmFrozen'): if key not in Controls: Controls[key] = {} parmFrozen = Controls['parmFrozen'] - if G2frame.testSeqRefineMode(): + if bool(G2frame.testSeqRefineMode(True)): frozenList = set() for h in parmFrozen: if h == 'FrozenList': continue @@ -8603,7 +8805,7 @@ def OnMergeHKL(event): else: print( 'nothing to merge for %s reflections'%(mergeRef.shape[0])) HKLFlist = [] - newName = Name+u' '+Laue + newName = f'{Name} {Laue}' if G2frame.GPXtree.GetCount(): item, cookie = G2frame.GPXtree.GetFirstChild(G2frame.root) while item: @@ -8900,13 +9102,13 @@ def OnEditMag(**args): num = len(data[1][0]) if 'TOF' in data[0].get('simType','CW'): step = (np.log(Tmax) - np.log(Tmin))/(num-1.) - t = u'\u00b5s' - lbl = u'Simulation range: {:.2f} to {:.2f} {:s} with {:.4f} resolution ({:d} points)' + t = '\u00b5s' + lbl = 'Simulation range: {:.2f} to {:.2f} {:s} with {:.4f} resolution ({:d} points)' else: step = (Tmax - Tmin)/(num-1) - t = u'2\u03b8' # 2theta - lbl = u'Simulation range: {:.2f} to {:.2f} {:s} with {:.4f} steps ({:d} points)' - lbl += u'\n(Edit range resets observed intensities).' + t = '2\u03b8' # 2theta + lbl = 'Simulation range: {:.2f} to {:.2f} {:s} with {:.4f} steps ({:d} points)' + lbl += '\n(Edit range resets observed intensities).' lbl = lbl.format(Tmin,Tmax,t,step,num) simSizer.Add(wx.StaticText(G2frame.dataWindow,wx.ID_ANY,lbl),0,WACV) but = wx.Button(G2frame.dataWindow,wx.ID_ANY,"Edit range") @@ -8938,15 +9140,15 @@ def OnEditMag(**args): name = data[0].get(pfx.split(':')[0]+'::Name','?') if 'SS' in value: mainSizer.Add((5,5),) - mainSizer.Add(wx.StaticText(G2frame.dataWindow,-1,u' For incommensurate phase '+name+u':')) + mainSizer.Add(wx.StaticText(G2frame.dataWindow,-1,' For incommensurate phase '+name+':')) for m,(Rf2,Rf,Nobs) in enumerate(zip(data[0][pfx+'Rf^2'],data[0][pfx+'Rf'],data[0][value])): mainSizer.Add(wx.StaticText(G2frame.dataWindow,-1, - u' m = +/- %d: RF\u00b2: %.3f%%, RF: %.3f%% on %d reflections '%(m,Rf2,Rf,Nobs))) + ' m = +/- %d: RF\u00b2: %.3f%%, RF: %.3f%% on %d reflections '%(m,Rf2,Rf,Nobs))) else: mainSizer.Add((5,5),) - mainSizer.Add(wx.StaticText(G2frame.dataWindow,-1,u' For phase '+name+u':')) + mainSizer.Add(wx.StaticText(G2frame.dataWindow,-1,' For phase '+name+':')) mainSizer.Add(wx.StaticText(G2frame.dataWindow,-1, - u' Unweighted phase residuals RF\u00b2: %.3f%%, RF: %.3f%% on %d reflections '% \ + ' Unweighted phase residuals RF\u00b2: %.3f%%, RF: %.3f%% on %d reflections '% \ (data[0][pfx+'Rf^2'],data[0][pfx+'Rf'],data[0][value]))) # Draw edit box for Magnification factors/positions @@ -9152,7 +9354,7 @@ def OnShowShift(event): G2frame.GetStatusBar().SetStatusText('',1) SetDataMenuBar(G2frame) - G2frame.SetTitleByGPX() + G2frame.SetTitleByGPX() # probably not needed as this is set when a project is read (G2miscGUI.ProjFileOpen) G2frame.PickId = item G2frame.PickIdText = None parentID = G2frame.root @@ -9389,6 +9591,11 @@ def OnShowShift(event): #import imp #imp.reload(G2ddG) G2ddG.MakeHistPhaseWin(G2frame) + elif G2frame.GPXtree.GetItemText(item).startswith('Groups/'): + # groupDict is defined (or item would not be in tree). + # At least for now, this does nothing, so advance to first group entry + item, cookie = G2frame.GPXtree.GetFirstChild(item) + wx.CallAfter(G2frame.GPXtree.SelectItem,item) elif GSASIIpath.GetConfigValue('debug'): print('Unknown tree item',G2frame.GPXtree.GetItemText(item)) ############################################################################ @@ -9590,6 +9797,16 @@ def OnShowShift(event): data = G2frame.GPXtree.GetItemPyData(G2frame.PatternId) G2pdG.UpdateReflectionGrid(G2frame,data,HKLF=True,Name=name) G2frame.dataWindow.HideShow.Enable(True) + elif G2frame.GPXtree.GetItemText(parentID).startswith('Groups/'): + # if GSASIIpath.GetConfigValue('debug'): + # print('Debug: reloading',G2gr) + # from importlib import reload + # reload(G2pwpl) + # reload(G2gr) + G2gr.UpdateGroup(G2frame,item) + elif GSASIIpath.GetConfigValue('debug'): + print(f'Unknown subtree item {G2frame.GPXtree.GetItemText(item)!r}', + f'\n\tparent: {G2frame.GPXtree.GetItemText(parentID)!r}') if G2frame.PickId: G2frame.PickIdText = G2frame.GetTreeItemsList(G2frame.PickId) @@ -9644,7 +9861,7 @@ def SetDataMenuBar(G2frame,menu=None): # lists of menu items that need to be changed: # ExportPDF, MakePDF, ExportMTZ, ExportPeakList, ExportHKL # Refine, ExportSeq, ExportNonSeq - G2frame.testSeqRefineMode() # sets items in Refine, ExportSeq, ExportNonSeq + G2frame.testSeqRefineMode(False) # sets items in Refine, ExportSeq, ExportNonSeq # now extend the menu item status in 1st menu to the duplicates of that menu # item in other menus for obj in (G2frame.ExportPDF, G2frame.MakePDF, diff --git a/GSASII/GSASIIddataGUI.py b/GSASII/GSASIIddataGUI.py index 6cda9589..981a2a6d 100644 --- a/GSASII/GSASIIddataGUI.py +++ b/GSASII/GSASIIddataGUI.py @@ -961,7 +961,7 @@ def OnFixVals(event): useBox.Add(lebail,0,WACV) bottomSizer.Add(useBox) G2G.HorizontalLine(bottomSizer,DData) - if G2frame.testSeqRefineMode() and not UseList[G2frame.hist]['LeBail']: + if bool(G2frame.testSeqRefineMode(True)) and not UseList[G2frame.hist]['LeBail']: bottomSizer.Add(wx.StaticText(DData,label=' Sequential Refinement Options')) parmChoice = [' ','X','XU','U','F','FX','FXU','FU'] if generalData['Type'] == 'magnetic': diff --git a/GSASII/GSASIIexprGUI.py b/GSASII/GSASIIexprGUI.py index 9dd06b97..26a0885f 100644 --- a/GSASII/GSASIIexprGUI.py +++ b/GSASII/GSASIIexprGUI.py @@ -669,6 +669,9 @@ def Repaint(self,exprObj): except Exception as msg: self.setEvalResult(f'Error in evaluation: {msg}') return + if val is None: + self.setEvalResult("Expression value is None") + return if not np.isfinite(val): self.setEvalResult("Expression value is infinite or out-of-bounds") return diff --git a/GSASII/GSASIIgroupGUI.py b/GSASII/GSASIIgroupGUI.py new file mode 100644 index 00000000..6da1700c --- /dev/null +++ b/GSASII/GSASIIgroupGUI.py @@ -0,0 +1,1297 @@ +# -*- coding: utf-8 -*- +''' +GSASIIgroupGUI: Routines for working with groups of histograms. + +Outline of code in GSASIIgroupGUI: + +* :func:`SearchGroups` is used to define which histograms are in each + group. Defines variables in Controls['Groups'] (see below) + +* :func:`UpdateGroup` is used to create the contents of the DataWindow + +* :func:`HistFrame` is called by :func:`UpdateGroup` to display the + main contents of the window + +* :func:`displayDataArray` is called by :func:`HistFrame` to create the + parameter table shown in the window, from the **Parameter Data Table** + entries, as described below. + +A few routines are optionally used to color parameters by the shift +applied in the last refinement. Routine :func:`computeShiftInfo` computes +the shifts for all parameters, creates a set of steps for them and a color +scale. All these are placed in global array ``shiftInfo``. The +function :func:`colorCaption` creates a caption showing the levels and colors +developed in :func:`computeShiftInfo`, while :func:`ColorTxtbox` applies +those colors to ValidatedTextCtrl entry widgets. + +The settings in Controls entry ['Groups'] that define how histograms are +divided into groups are: + +* Controls['Groups']['groupDict'] + a dict where each key is the name of the group and the value is a list of + histograms in the group +* Controls['Groups']['notGrouped'] + a count of the number of histograms that are not in any group +* Controls['Groups']['template'] + the string used to set the grouping +* Controls['Groups']['groupSeqSel'] + a list of the groups to be included in the next sequential fit, will + be a subset of the keys for Controls['Groups']['groupDict'] + +** Parameter Data Table ** + +For use in creating GUI tables and to copy values between histograms, +a dict is created in :func:`getSampleVals`, :func:`getInstVals`, +:func:`getLimitVals`, :func:`getBkgVals` or :func:`getHAPvals` with +the parameters used in that data tree item. The dict is organized +with the following entries: + + * dict['__dataSource'] : SourceArray + + * dict[histogram]['label'] : `innerdict` + +where `label` is the text shown on the row label and `innerdict` is a dict +containing one or more of the following elements: + + * 'val' : (key1,key2,...) + * 'ref' : (key1, key2,...) + * 'range' : (float,float) + * 'str' : (key1,key2,...) + * 'fmt' : str + * 'txt' : str + * 'init' : float + * 'rowlbl' : (array, key) + * 'setintfunc' : function + * 'prmname': str + +One of 'val', 'ref' or 'str' elements must be present. + + * The 'val' tuple provides a reference to the float value for the + defined quantity, where SourceArray[histogram][key1][key2][...] + provides r/w access to the parameter. + + * The 'ref' tuple provides a reference to the bool value, where + SourceArray[histogram][key1][key2][...] provides r/w access to the + refine flag for the labeled quantity. + + Both 'ref' and 'val' are usually defined together, but either may + occur alone. These exceptions will be for parameters where a single + refine flag is used for a group of parameters or for non-refined + parameters. + + Note that + :func:`indexArrayVal` is used to obtain the value for the + quantity from the keys in the tuple for the 'ref' and 'val' entries, + while :func:`indexArrayRef` + is used to obtain the dict/list reference and the key/index for that + object from the saved info (so that the value can be changed). + + * The 'str' value is something that cannot be edited from the GUI; If 'str' is + present, the only other possible entries that can be present is either 'fmt' + or 'txt. + 'str' is used for a parameter value that is typically computed or + cannot be edited in the group tables (edit by selecting the + appropriate in the histogram tree entry. + +Optional elements in the array: + + * The 'fmt' value is a string used to format a 'str' value to + convert it to a string. Used for float or int values. + + * The 'txt' value is a string that replaces the value in 'str'. + + * The 'init' value is also something that cannot be edited. + These 'init' values are used for Instrument Parameters + where there is both a current value for the parameter as + well as an initial value (usually read from the instrument + parameters file when the histogram is read. If 'init' is + present in `innerdict`, there will also be a 'val' entry + in `innerdict` and likely a 'ref' entry as well. + + * The 'rowlbl' value provides a reference to a str value that + will be an editable row label (FreePrmX sample parametric + values). + + * The 'range' list/tuple provides min and max float value for the + defined quantity to be defined. Use None for any value that + should not be enforced. The 'range' values will be used as limits + for the entry widget. + + * The 'setintfunc' value defines a function that is executed when the + 'ref' tuple is changed. When this is supplied, a spin box is used + to set the value rather than a TextCtrl. + + * The 'prmname' name entry lists the parameter name, as used in + varyList, parmDict etc. + +The full list of routines used in mod:`GSASIIgroupGUI` are: +''' + +import math +import re +import wx + +from . import GSASIIdataGUI as G2gd +from . import GSASIIspc as G2spc +from . import GSASIIlattice as G2lat +from . import GSASIIctrlGUI as G2G +from . import GSASIIpwdplot as G2pwpl + +WACV = wx.ALIGN_CENTER_VERTICAL + +shiftInfo = {'colorMode':False} # home for into to color TextCtrls + +def SearchGroups(G2frame,Histograms,hist): + '''Determine which histograms are in groups, called by SearchGroups in + :func:`GSASIIdataGUI.UpdateControls`. + ''' + repeat = True + srchStr = hist[5:] + msg = ('Edit the histogram name below placing a question mark (?) ' + 'at the location ' + 'of characters that change between groups of ' + 'histograms. Use backspace or delete to remove ' + 'characters that should be ignored as they will ' + 'vary within a histogram group (e.g. Bank 1). ' + 'Be sure to leave enough characters so the string ' + 'can be uniquely matched.') + while repeat: + srchStr = G2G.StringSearchTemplate(G2frame,'Set match template', + msg,srchStr) + if srchStr is None: return {} # cancel pressed + reSrch = re.compile(srchStr.replace('.',r'\.').replace('?','.')) + setDict = {} + keyList = [] + noMatchCount = 0 + for hist in Histograms: + if hist.startswith('PWDR '): + m = reSrch.search(hist) + if m: + key = hist[m.start():m.end()] + setDict[hist] = key + if key not in keyList: keyList.append(key) + else: + noMatchCount += 1 + groupDict = {} + groupCount = {} + for k in keyList: + groupDict[k] = [hist for hist,key in setDict.items() if k == key] + groupCount[k] = len(groupDict[k]) + + msg1 = f'With search template "{srchStr}", ' + + buttons = [] + OK = True + if len(groupCount) == 0: + msg1 += f'there are ho histograms in any groups.' + elif min(groupCount.values()) == max(groupCount.values()): + msg1 += f'there are {len(groupDict)} groups with {min(groupCount.values())} histograms each.' + else: + msg1 += (f'there are {len(groupDict)} groups with between {min(groupCount.values())}' + f' and {min(groupCount.values())} histograms in each.') + if noMatchCount: + msg1 += f"\n\nNote that {noMatchCount} PWDR histograms were not included in any groups." + # place a sanity check limit on the number of histograms in a group + if len(groupCount) == 0: + OK = False + elif max(groupCount.values()) >= 150: + OK = False + msg1 += '\n\nThis exceeds the maximum group length of 150 histograms' + elif min(groupCount.values()) == max(groupCount.values()) == 1: + OK = False + msg1 += '\n\nEach histogram is in a separate group. Grouping histograms only makes sense with multiple histograms in at least some groups.' + if OK: + buttons += [('OK', lambda event: event.GetEventObject().GetParent().EndModal(wx.ID_OK))] + buttons += [('try again', lambda event: event.GetEventObject().GetParent().EndModal(wx.ID_CANCEL))] + res = G2G.ShowScrolledInfo(G2frame,msg1,header='Grouping result', + buttonlist=buttons,height=150) + if res == wx.ID_OK: + repeat = False + + return {'groupDict':groupDict,'notGrouped':noMatchCount,'template':srchStr} + +def UpdateGroup(G2frame,selectedGrp,plot=True): + '''Code to display parameters from all sets of grouped histograms + ''' + def OnColorMode(event): + if event.GetId() == G2G.wxID_GRPLOG: + shiftInfo['colorMode'] = 'log' + elif event.GetId() == G2G.wxID_GRPLIN: + shiftInfo['colorMode'] = 'linear' + else: + shiftInfo['colorMode'] = False + wx.CallAfter(UpdateGroup,G2frame,selectedGrp,False) + + def onDisplaySel(event): + '''Change the type of parameters that are shown in response to the + pulldown in the upper left + ''' + G2frame.GroupInfo['displayMode'] = dsplType.GetValue() + wx.CallAfter(UpdateGroup,G2frame,selectedGrp,False) + + def onPhaseSel(event): + '''Change the selected phase (in Hist/Phase mode only) + ''' + G2frame.GroupInfo['selectedPhase'] = G2frame.GroupInfo['phaseSel'].GetValue() + wx.CallAfter(UpdateGroup,G2frame,selectedGrp,False) + + def copyPrep(): + '''Prepare to copy parameters by locating groups that have the + same number of histograms as the currently selected group and + then select from those groups. Initiated by copy all/copy selected + menu command. + + Could check even more closely that histograms match up properly, + but for now I don't think that is needed. + ''' + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + groupName = G2frame.GroupInfo['groupName'] + # make a list of groups of the same length as the current + curLen = len(groupDict[groupName]) + matchGrps = [] + selList = [] + for g in groupDict: + if g == groupName: continue + if curLen != len(groupDict[g]): continue + matchGrps.append(g) + if len(matchGrps) == 0: + G2G.G2MessageBox(G2frame, + f'No groups found with {curLen} histograms', + 'No matching groups') + return + elif len(matchGrps) > 1: + dlg = G2G.G2MultiChoiceDialog(G2frame, 'Copy to which groups?', 'Copy to?', matchGrps) + try: + if dlg.ShowModal() == wx.ID_OK: + selList = [matchGrps[i] for i in dlg.GetSelections()] + finally: + dlg.Destroy() + else: + selList = matchGrps + if len(selList) == 0: return + return selList,groupDict,groupName + + def OnCopyAll(event): + '''Copy all displayed parameters to the selected histograms. + + Called by menu command. + ''' + res = copyPrep() + if res is None: return + selList,groupDict,groupName = res + dataSource = prmArray['_dataSource'] + for h in selList: # selected groups + for src,dst in zip(groupDict[groupName],groupDict[h]): # histograms in groups (same length enforced) + for i in prmArray[src]: + for j in ('ref','val','str'): + if j in prmArray[src][i]: + if prmArray[src][i][j] is None: continue + try: + arr,indx = indexArrayRef(dataSource,dst,prmArray[src][i][j]) + arr[indx] = indexArrayVal(dataSource,src,prmArray[src][i][j]) + # if changing the number of background terms, the array length needs to change too + if 'setintfunc' in prmArray[src][i]: + prmArray[src][i]['setintfunc'](dst) + except Exception as msg: # could hit an error if an array element is not defined + pass + + def OnCopySel(event): + '''Copy selected parameters from a list based on what is displayed + to the selected histograms. + + Called by menu command. + ''' + res = copyPrep() + if res is None: return + selList,groupDict,groupName = res + dataSource = prmArray['_dataSource'] + choices = [] + for src in groupDict[groupName]: + for i in prmArray[src]: + for j in ('ref','val','str'): + if prmArray[src][i].get(j) is None: + continue + if i not in choices: + choices.append(i) + dlg = G2G.G2MultiChoiceDialog(G2frame, 'Copy which items?', 'Copy what?', choices) + itemList = [] + try: + if dlg.ShowModal() == wx.ID_OK: + itemList = [choices[i] for i in dlg.GetSelections()] + finally: + dlg.Destroy() + if len(itemList) == 0: return + for h in selList: # selected groups + for src,dst in zip(groupDict[groupName],groupDict[h]): # histograms in groups (same length enforced) + for i in prmArray[src]: + if i not in itemList: continue + for j in ('ref','val','str'): + if j in prmArray[src][i]: + if prmArray[src][i][j] is None: continue + try: + arr,indx = indexArrayRef(dataSource,dst,prmArray[src][i][j]) + arr[indx] = indexArrayVal(dataSource,src,prmArray[src][i][j]) + # if changing the number of background terms, the array length needs to change too + if 'setintfunc' in prmArray[src][i]: + prmArray[src][i]['setintfunc'](dst) + except Exception as msg: # could hit an error if an array element is not defined + pass + + ##### beginning of code to display Groups of Histograms ### + Histograms,Phases = G2frame.GetUsedHistogramsAndPhasesfromTree() + if not hasattr(G2frame,'GroupInfo'): + G2frame.GroupInfo = {} + computeShiftInfo(G2frame) # Setup for color by shift/sigma + # start with Sample but reuse the last displayed item + G2frame.GroupInfo['displayMode'] = G2frame.GroupInfo.get('displayMode','Sample') + #G2frame.GroupInfo['displayMode'] = G2frame.GroupInfo.get('displayMode','Hist/Phase') # debug code + G2frame.GroupInfo['groupName'] = G2frame.GPXtree.GetItemText(selectedGrp) + G2frame.GroupInfo['selectedPhase'] = G2frame.GroupInfo.get('selectedPhase', + list(Phases.keys())[0]) + G2frame.GroupInfo['Redraw'] = (UpdateGroup,G2frame,selectedGrp,False) + G2gd.SetDataMenuBar(G2frame,G2frame.dataWindow.GroupMenu) + G2frame.Bind(wx.EVT_MENU, OnCopyAll, id=G2G.wxID_GRPALL) + G2frame.Bind(wx.EVT_MENU, OnCopySel, id=G2G.wxID_GRPSEL) + for i in (G2G.wxID_GRPNONE, G2G.wxID_GRPLOG, G2G.wxID_GRPLIN): + G2frame.Bind(wx.EVT_MENU, OnColorMode, id=i) + G2frame.dataWindow.ClearData() + G2frame.dataWindow.helpKey = "Groups/Powder" + topSizer = G2frame.dataWindow.topBox + topParent = G2frame.dataWindow.topPanel + dsplType = wx.ComboBox(topParent,wx.ID_ANY, + value=G2frame.GroupInfo['displayMode'], + choices=['Hist/Phase','Sample','Instrument', + 'Instrument-\u0394', + 'Limits','Background'], + style=wx.CB_READONLY|wx.CB_DROPDOWN) + dsplType.Bind(wx.EVT_COMBOBOX, onDisplaySel) + topSizer.Add(dsplType,0,WACV) + G2frame.GroupInfo['phaseSel'] = wx.ComboBox(topParent,wx.ID_ANY, + value=G2frame.GroupInfo['selectedPhase'], + choices=list(Phases), + style=wx.CB_READONLY|wx.CB_DROPDOWN) + G2frame.GroupInfo['phaseSel'].Bind(wx.EVT_COMBOBOX, onPhaseSel) + topSizer.Add(G2frame.GroupInfo['phaseSel'],0,wx.RIGHT|wx.LEFT|WACV,10) + topSizer.Add(wx.StaticText(topParent, + label=f' Group "{histLabels(G2frame)[0]}" parameters'), + 0,WACV) + topSizer.Add((-1,-1),1,wx.EXPAND) + topSizer.Add(G2G.HelpButton(topParent,helpIndex=G2frame.dataWindow.helpKey)) + + HistFrame(G2frame,Histograms,Phases) + if plot: G2pwpl.PlotPatterns(G2frame,plotType='GROUP') + G2frame.dataWindow.SetDataSize() + wx.CallAfter(G2frame.SendSizeEvent) + +def histLabels(G2frame): + '''Find portion of the set of hist names that are the same for all + histograms in the current group (determined by ``G2frame.GroupInfo['groupName']``) + and then for each histogram, the characters that are different. + + :Returns: commonltrs, histlbls where + + * commonltrs is a str containing the letters shared by all + histograms in the group and where differing letters are + replaced by a square box. + * histlbls is a list with an str for each histogram listing + the characters that differ in each histogram. + ''' + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId( + G2frame,G2frame.root, 'Controls')) + groupName = G2frame.GroupInfo['groupName'] + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + l = max([len(i) for i in groupDict[groupName]]) + h0 = groupDict[groupName][0].ljust(l) + msk = [True] * l + for h in groupDict[groupName][1:]: + msk = [m & (h0i == hi) for h0i,hi,m in zip(h0,h.ljust(l),msk)] + # place rectangular box in the loc of non-common letter(s) + commonltrs = ''.join([h0i if m else '\u25A1' for (h0i,m) in zip(h0,msk)]) + # make list with histogram name unique letters + histlbls = [''.join([hi for (hi,m) in zip(h,msk) if not m]) + for h in groupDict[groupName]] + return commonltrs,histlbls + +def indexArrayRef(dataSource,hist,arrayIndices): + '''Find the array and key for a refinement flag. (From a 'ref' or + 'val' entry.) + + :returns: arr, indx where arr[indx] is the refinement + flag value (bool for 'ref') or the value (float for 'val') + ''' + indx = arrayIndices[-1] + arr = dataSource[hist] + for i in arrayIndices[:-1]: + arr = arr[i] + return arr,indx + +def indexArrayVal(dataSource,hist,arrayIndices): + '''Find the value for a GSAS-II parameter in the data tree + from a 'val' or the refinement flag from the entry + + :returns: the parameter value (usually float or bool). Will return + None if the arrayIndices list does not match the nested array/lists keys. + ''' + if arrayIndices is None: return None + arr = dataSource[hist] + for i in arrayIndices: + try: + arr = arr[i] + except: + return None + return arr + +ClearAllLbl = '☐' +SetAllLbl = '☑' +def onRefineAll(event): + '''Respond to the Refine All button. On the first press, all + refine check buttons are set as "on" and the button is relabeled + as ClearAllLbl (for clear). On the second press, all refine check + buttons are set as "off" and the button is relabeled as SetAllLbl (for + set). + ''' + but = event.GetEventObject() + dataSource = but.refDict['dataSource'] + checkButList = but.checkButList + + if but.GetLabelText() == SetAllLbl: + setting = True + but.SetLabelText(ClearAllLbl) + else: + setting = False + but.SetLabelText(SetAllLbl) + for c in checkButList: + c.SetValue(setting) + for item,hist in zip(but.refDict['arrays'],but.refDict['hists']): + arr,indx = indexArrayRef(dataSource,hist,item) + arr[indx] = setting + +def onSetAll(event): + '''Respond to the copy right button. Copies the first value to + all other edit widgets in that row. + ''' + but = event.GetEventObject() + firstVal = indexArrayVal(but.valDict['dataSource'], + but.valDict['hists'][0],but.valDict['arrays'][0]) + for f in but.valDict['valSetFxnList']: + f(firstVal) + +def onResetAll(event): + '''Respond to the Reset button. Copies the initial setting (which + should be index 0) to the current setting (which should be index 1) + for all histograms in the current row. This is only used for + instrument parameters. + ''' + but = event.GetEventObject() + dataSource = but.valDict['dataSource'] + for item,hist in zip(but.valDict['arrays'],but.valDict['hists']): + arr,indx = indexArrayRef(dataSource,hist,item) + if indx == 1: + arr[1] = arr[0] + G2frame = but.GetTopLevelParent() + if G2frame.GroupInfo.get('Redraw'): + wx.CallAfter(*G2frame.GroupInfo.get('Redraw')) + +def displayDataArray(rowLabels,DataArray,Sizer,Panel,deltaMode=False, + CopyCtrl=True): + '''Displays the data table in `DataArray` in Scrolledpanel `Panel` + with wx.FlexGridSizer `Sizer`. + ''' + def OnSpin(evt): + '''respond when a SpinButton entry is changed (currently used for + background terms only) + ''' + spin = (evt.GetEventObject()) + spin.arr[spin.indx] = evt.GetEventObject().GetValue() + spin.txt.SetLabel(str(spin.arr[spin.indx])) + spin.setTermsFnx() + firstentry = None + checkButList = {} + valSetFxnList = {} + lblDict = {} + dataSource = DataArray['_dataSource'] + for row in rowLabels: + checkButList[row] = [] + valSetFxnList[row] = [] + # show the row labels + refList = [] + valList = [] + for hist in DataArray: + if row not in DataArray[hist]: continue + if 'val' in DataArray[hist][row]: + valList.append(DataArray[hist][row]['val']) + if 'ref' in DataArray[hist][row]: + refList.append(DataArray[hist][row]['ref']) + arr = None + histList = [] + for hist in DataArray: + if row not in DataArray[hist]: continue + histList.append(hist) + if 'rowlbl' in DataArray[hist][row]: + arr,key = DataArray[hist][row]['rowlbl'] + break + if arr is None: # text row labels + w = wx.StaticText(Panel,label=row) + else: # used for "renameable" sample vars (str) + w = G2G.ValidatedTxtCtrl(Panel,arr,key,size=(125,-1)) + Sizer.Add(w,0,wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) + lblDict[row] = w + + if deltaMode: + resetBut = wx.Button(Panel,label='↩',style=wx.BU_EXACTFIT) + resetBut.valDict = {'arrays': valList, 'hists': histList, + 'dataSource':dataSource} + Sizer.Add(resetBut,0,wx.ALIGN_CENTER_VERTICAL) + resetBut.Bind(wx.EVT_BUTTON,onResetAll) + + if len(refList) > 2: # >two refinement flags in this row. Include ste/clear all button + lbl = SetAllLbl + if all([indexArrayVal(dataSource,hist,i) for i in refList]): lbl = ClearAllLbl + refAll = wx.Button(Panel,label=lbl,style=wx.BU_EXACTFIT) + font = refAll.GetFont() + font.PointSize += 5 + refAll.SetFont(font) + refAll.refDict = {'arrays': refList, 'hists': histList, + 'dataSource':dataSource} + refAll.checkButList = checkButList[row] + Sizer.Add(refAll,0,wx.ALIGN_CENTER_VERTICAL) + refAll.Bind(wx.EVT_BUTTON,onRefineAll) + else: + Sizer.Add((-1,-1)) + + i = -1 + for hist in DataArray: + if hist == '_dataSource': continue + i += 1 + if i == 1 and len(valList) > 2 and not deltaMode and CopyCtrl: + # right arrow copy button; place after 0th column + but = wx.Button(Panel,wx.ID_ANY,'\u2192',style=wx.BU_EXACTFIT) + but.valDict = {'arrays': valList, 'hists': histList, + 'dataSource':dataSource, + 'valSetFxnList' :valSetFxnList[row]} + Sizer.Add(but,0,wx.ALIGN_CENTER_VERTICAL) + but.Bind(wx.EVT_BUTTON,onSetAll) + elif i == 1 and CopyCtrl: + Sizer.Add((-1,-1)) + minval = None + maxval = None + # format the entry depending on what is defined + if row not in DataArray[hist]: + Sizer.Add((-1,-1)) + continue + elif 'range' in DataArray[hist][row]: + minval, maxval = DataArray[hist][row]['range'] + if ('init' in DataArray[hist][row] and + deltaMode and 'ref' in DataArray[hist][row]): + # instrument parameter (has 'init' value) and in "delta" + # mode: show difference + arr,indx = indexArrayRef(dataSource,hist,DataArray[hist][row]['val']) + delta = arr[indx] + arr,indx = DataArray[hist][row]['init'] + delta -= arr[indx] + if abs(delta) < 9e-6: delta = 0. + if delta == 0: + deltaS = "" + else: + deltaS = f"\u0394 {delta:.4g} " + valrefsiz = wx.BoxSizer(wx.HORIZONTAL) + valrefsiz.Add(wx.StaticText(Panel,label=deltaS),0) + arr,indx = indexArrayRef(dataSource,hist,DataArray[hist][row]['ref']) + w = G2G.G2CheckBox(Panel,'',arr,indx) + valrefsiz.Add(w,0,wx.ALIGN_CENTER_VERTICAL) + checkButList[row].append(w) + Sizer.Add(valrefsiz,0, + wx.EXPAND|wx.ALIGN_RIGHT) + elif 'init' in DataArray[hist][row] and deltaMode: + # a non-refined instrument parameter that has an initial value + # I don't think this ever happens + arr,indx = indexArrayRef(dataSource,hist,DataArray[hist][row]['val']) + delta = arr[indx] + arr,indx = DataArray[hist][row]['init'] + delta -= arr[indx] + if delta == 0: + deltaS = "" + else: + deltaS = f"\u0394 {delta:.4g} " + Sizer.Add(wx.StaticText(Panel,label=deltaS),0, + wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) + elif 'setintfunc' in DataArray[hist][row]: + # integer value that has a function to be called when changed + # GUI: provide a Spin Box to increment or decrement + # Currently used for background terms only. + spinsiz = wx.BoxSizer(wx.HORIZONTAL) + spin = wx.SpinButton(Panel, wx.ID_ANY) + spin.Bind(wx.EVT_SPIN, OnSpin) + spin.txt = wx.StaticText(Panel,label='?') + spinsiz.Add(spin.txt,0,wx.ALIGN_CENTER_VERTICAL) + spinsiz.Add((5,-1)) + spinsiz.Add(spin,0,wx.ALIGN_CENTER_VERTICAL) + Sizer.Add(spinsiz,0,wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER) + + spin.SetRange(1,36) + spin.arr,spin.indx = indexArrayRef(dataSource,hist,DataArray[hist][row]['val']) + spin.setTermsFnx = DataArray[hist][row]['setintfunc'] + spin.SetValue(spin.arr[spin.indx]) + spin.txt.SetLabel(str(spin.arr[spin.indx])) + def SetVal(newval,spin=spin): + 'Used to set a value for the current spinbutton & associated StaticText' + spin.arr[spin.indx] = newval + spin.SetValue(spin.arr[spin.indx]) + spin.txt.SetLabel(str(newval)) + spin.setTermsFnx() + valSetFxnList[row].append(SetVal) + elif 'val' in DataArray[hist][row] and 'ref' in DataArray[hist][row]: + # a value that has a refine flag + valrefsiz = wx.BoxSizer(wx.HORIZONTAL) + arr,indx = indexArrayRef(dataSource,hist,DataArray[hist][row]['val']) + txtctrl = G2G.ValidatedTxtCtrl(Panel,arr,indx,size=(80,-1), + nDig=[9,7,'g'], + xmin=minval,xmax=maxval) + valSetFxnList[row].append(txtctrl.ChangeValue) + valrefsiz.Add(txtctrl,0,WACV) + if firstentry is None: firstentry = txtctrl + arr,indx = indexArrayRef(dataSource,hist,DataArray[hist][row]['ref']) + w = G2G.G2CheckBox(Panel,'',arr,indx) + valrefsiz.Add(w,0,wx.ALIGN_CENTER_VERTICAL) + checkButList[row].append(w) + Sizer.Add(valrefsiz,0, + wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + ColorTxtbox(txtctrl,DataArray[hist][row],row) + elif 'val' in DataArray[hist][row]: + # a value that is not refined, but can be edited + arr,indx = indexArrayRef(dataSource,hist,DataArray[hist][row]['val']) + nDig = [9,7,'g'] + if type(arr[indx]) is str: nDig = None + w = G2G.ValidatedTxtCtrl(Panel,arr,indx,size=(80,-1), + nDig=nDig, + xmin=minval,xmax=maxval,notBlank=False) + valSetFxnList[row].append(w.ChangeValue) + Sizer.Add(w,0,wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + if firstentry is None: firstentry = w + ColorTxtbox(w,DataArray[hist][row],row) + + elif 'ref' in DataArray[hist][row]: + # a refinement flag for a group of parameters + arr,indx = indexArrayRef(dataSource,hist,DataArray[hist][row]['ref']) + w = G2G.G2CheckBox(Panel,'',arr,indx) + Sizer.Add(w,0,wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER) + checkButList[row].append(w) + elif 'str' in DataArray[hist][row]: + # a value that is not refined and cannot be edited + val = indexArrayVal(dataSource,hist,DataArray[hist][row]['str']) + if 'txt' in DataArray[hist][row]: + val = DataArray[hist][row]['txt'] + elif 'fmt' in DataArray[hist][row]: + f = DataArray[hist][row]['fmt'] + val = f'{val:{f}}' + Sizer.Add(wx.StaticText(Panel,label=val),0, + wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_CENTER) + else: + print('Should not happen',DataArray[hist][row],hist,row) + return firstentry,lblDict + +def HistFrame(G2frame,Histograms,Phases): + '''Display all the parameters of the selected type. + This puts everything in a single FlexGridSizer. + ''' + #--------------------------------------------------------------------- + # generate a dict with values for each histogram + CopyCtrl = True + global prmArray + G2frame.GroupInfo['phaseSel'].Show(False) + if G2frame.GroupInfo['displayMode'].startswith('Sample'): + prmArray = getSampleVals(G2frame,Histograms) + elif G2frame.GroupInfo['displayMode'].startswith('Instrument'): + prmArray = getInstVals(G2frame,Histograms) + elif G2frame.GroupInfo['displayMode'].startswith('Limits'): + CopyCtrl = False + prmArray = getLimitVals(G2frame,Histograms) + elif G2frame.GroupInfo['displayMode'].startswith('Background'): + prmArray = getBkgVals(G2frame,Histograms) + CopyCtrl = True + elif G2frame.GroupInfo['displayMode'].startswith('Hist/Phase') and Phases: + phase = G2frame.GroupInfo.get('selectedPhase',list(Phases.keys())[0]) + prmArray = getHAPvals(G2frame,phase,Histograms,Phases) + G2frame.GroupInfo['phaseSel'].Show(True) + else: + prmArray = None + print('Unexpected', G2frame.GroupInfo['displayMode']) + return + rowLabels = [] + lpos = 0 + nonZeroRows = [] + dataSource = prmArray['_dataSource'] + for hist in prmArray: + if hist == '_dataSource': continue + #cols = len(prmArray) + prevkey = None + for key in prmArray[hist]: + # find delta-terms that are non-zero + if '\u0394' in G2frame.GroupInfo['displayMode']: + if 'val' in prmArray[hist][key] and 'init' in prmArray[hist][key]: + arr,indx = prmArray[hist][key]['init'] + val = indexArrayVal(dataSource,hist,prmArray[hist][key]['val']) + if val is not None and abs(val-arr[indx]) > 1e-5: + nonZeroRows.append(key) + if key not in rowLabels: + if prevkey is None: + rowLabels.insert(lpos,key) + lpos += 1 + else: + rowLabels.insert(rowLabels.index(prevkey)+1,key) + prevkey = key + n = 2 # extra columns + # remove rows where delta-terms are all zeros + if '\u0394' in G2frame.GroupInfo['displayMode']: + rowLabels = [i for i in rowLabels if i in nonZeroRows] + n += 1 + #======= Generate GUI =============================================== + # layout the window + midPanel = G2frame.dataWindow + mainSizer = wx.BoxSizer(wx.VERTICAL) + G2G.HorizontalLine(mainSizer,midPanel) + midPanel.SetSizer(mainSizer) + deltaMode = "\u0394" in G2frame.GroupInfo['displayMode'] + if CopyCtrl and len(prmArray) > 2: n += 1 # add column for copy (when more than one histogram) + if deltaMode and not rowLabels: + mainSizer.Add(wx.StaticText(midPanel, + label='No parameters deviate from initial values'), + 0,wx.ALL,10) + elif not rowLabels: # I don't think this can happen + mainSizer.Add(wx.StaticText(midPanel,label='No parameters to display')) + else: + valSizer = wx.FlexGridSizer(0,len(prmArray)+n-1,3,10) + mainSizer.Add(valSizer,1,wx.EXPAND) + # place column headers into table + valSizer.Add(wx.StaticText(midPanel,label=' ')) + if '\u0394' in G2frame.GroupInfo['displayMode']: + valSizer.Add(wx.StaticText(midPanel,label='Reset')) + valSizer.Add(wx.StaticText(midPanel,label=' Ref ')) + for i,hist in enumerate(histLabels(G2frame)[1]): + if i == 1 and CopyCtrl: + if deltaMode: + valSizer.Add((-1,-1)) + elif CopyCtrl: + valSizer.Add(wx.StaticText(midPanel,label=' Copy ')) + valSizer.Add(wx.StaticText(midPanel, + label=f"\u25A1 = {hist}"), + 0,wx.ALIGN_CENTER) + # fill the remaining rows in the table + firstentry,lblDict = displayDataArray(rowLabels,prmArray,valSizer,midPanel, + deltaMode=deltaMode,CopyCtrl=CopyCtrl) + + if firstentry is not None: # prevent scroll to show last entry + wx.Window.SetFocus(firstentry) + firstentry.SetInsertionPoint(0) # prevent selection of text in widget + colorCaption(G2frame) + +def getSampleVals(G2frame,Histograms): + '''Generate the Parameter Data Table (a dict of dicts) with + all Sample values for all histograms in the + selected histogram group (from G2frame.GroupInfo['groupName']). + This will be used to generate the contents of the GUI for Sample values. + ''' + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupName = G2frame.GroupInfo['groupName'] + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + if groupName not in groupDict: + print(f'Unexpected: {groupName} not in groupDict') + return + # parameters to include in table + parms = [] + indexDict = {'_dataSource':Histograms} + def histderef(hist,l): + a = Histograms[hist] + for i in l: + a = a[i] + return a + # loop over histograms in group + for hist in groupDict[groupName]: + prmPrefix = f':{Histograms[hist]['hId']}:' + indexDict[hist] = {} + indexDict[hist]['Inst. name'] = { + 'val' : ('Sample Parameters','InstrName')} + indexDict[hist]['Diff type'] = { + 'str' : ('Sample Parameters','Type')} + indexDict[hist]['Scale factor'] = { + 'val' : ('Sample Parameters','Scale',0), + 'ref' : ('Sample Parameters','Scale',1), + 'prmname': prmPrefix+'Scale', + } + # make a list of parameters to show + histType = Histograms[hist]['Instrument Parameters'][0]['Type'][0] + dataType = Histograms[hist]['Sample Parameters']['Type'] + if histType[2] in ['A','B','C']: + parms.append(['Gonio. radius','Gonio radius','.3f']) + #if 'PWDR' in histName: + if dataType == 'Debye-Scherrer': + if 'T' in histType: + parms += [['Absorption','Sample abs, \xb5r/\u03bb',None,'Absorption']] + else: + parms += [ + ['DisplaceX','Sample X displ',None,'DisplaceX'], + ['DisplaceY','Sample Y displ',None,'DisplaceY'], + ['Absorption','Sample abs,\xb5\xb7r',None,'Absorption']] + elif dataType == 'Bragg-Brentano': + parms += [ + ['Shift','Sample displ',None,'Shift'], + ['Transparency','Sample transp',None,'Transparency'], + ['SurfRoughA','Surf rough A',None,'SurfRoughA'], + ['SurfRoughB','Surf rough B',None,'SurfRoughB']] + #elif 'SASD' in histName: + # parms.append(['Thick','Sample thickness (mm)',[10,3]]) + # parms.append(['Trans','Transmission (meas)',[10,3]]) + # parms.append(['SlitLen',u'Slit length (Q,\xc5'+Pwrm1+')',[10,3]]) + parms.append(['Omega','Gonio omega',None]) + parms.append(['Chi','Gonio chi',None]) + parms.append(['Phi','Gonio phi',None]) + parms.append(['Azimuth','Detect azimuth',None]) + parms.append(['Time','time',None]) + parms.append(['Temperature','Sample T',None]) + parms.append(['Pressure','Sample P',None]) + + # and loop over them + for entry in parms: + prmNam = None + if len(entry) == 3: + key,lbl,fmt = entry + else: + key,lbl,fmt,prmNam = entry + if fmt is None and type(Histograms[hist]['Sample Parameters'][key]) is list: + indexDict[hist][lbl] = { + 'val' : ('Sample Parameters',key,0), + 'ref' : ('Sample Parameters',key,1),} + if prmNam: + indexDict[hist][lbl]['prmname'] = prmPrefix+prmNam + + elif fmt is None: + indexDict[hist][lbl] = { + 'val' : ('Sample Parameters',key)} + elif type(fmt) is str: + indexDict[hist][lbl] = { + 'str' : ('Sample Parameters',key), + 'fmt' : fmt} + + for key in ('FreePrm1','FreePrm2','FreePrm3'): + lbl = Controls[key] + indexDict[hist][lbl] = { + 'val' : ('Sample Parameters',key), + 'rowlbl' : (Controls,key) + } + return indexDict + +def getInstVals(G2frame,Histograms): + '''Generate the Parameter Data Table (a dict of dicts) with + all Instrument Parameter values for all histograms in the + selected histogram group (from G2frame.GroupInfo['groupName']). + This will be used to generate the contents of the GUI values. + ''' + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupName = G2frame.GroupInfo['groupName'] + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + if groupName not in groupDict: + print(f'Unexpected: {groupName} not in groupDict') + return + # parameters to include in table + indexDict = {'_dataSource':Histograms} + # loop over histograms in group + for hist in groupDict[groupName]: + prmPrefix = f':{Histograms[hist]['hId']}:' + indexDict[hist] = {} + insVal = Histograms[hist]['Instrument Parameters'][0] + insType = insVal['Type'][1] + if 'Bank' in Histograms[hist]['Instrument Parameters'][0]: + indexDict[hist]['Bank'] = { + 'str' : ('Instrument Parameters',0,'Bank',1), + 'fmt' : '.0f' + } + indexDict[hist]['Hist type'] = { + 'str' : ('Instrument Parameters',0,'Type',1), + } + if insType[2] in ['A','B','C']: #constant wavelength + keylist = [('Azimuth','Azimuth','.3f'),] + if 'Lam1' in insVal: + keylist += [('Lam1','Lambda 1','.6f'), + ('Lam2','Lambda 2','.6f'), + (['Source',1],'Source','s'), + ('I(L2)/I(L1)','I(L2)/I(L1)',None)] + else: + keylist += [('Lam','Lambda',None),] + itemList = ['Zero','Polariz.'] + if 'C' in insType: + itemList += ['U','V','W','X','Y','Z','SH/L'] + elif 'B' in insType: + itemList += ['U','V','W','X','Y','Z','alpha-0','alpha-1','beta-0','beta-1'] + else: #'A' + itemList += ['U','V','W','X','Y','Z','alpha-0','alpha-1','beta-0','beta-1','SH/L'] + for lbl in itemList: + keylist += [(lbl,lbl,None),] + elif 'E' in insType: + for lbl in ['XE','YE','ZE','WE']: + keylist += [(lbl,lbl,'.6f'),] + for lbl in ['A','B','C','X','Y','Z']: + keylist += [(lbl,lbl,None),] + elif 'T' in insType: + keylist = [('fltPath','Flight path','.3f'), + ('2-theta','2\u03B8','.2f'),] + for lbl in ['difC','difA','difB','Zero','alpha', + 'beta-0','beta-1','beta-q', + 'sig-0','sig-1','sig-2','sig-q','X','Y','Z']: + keylist += [(lbl,lbl,None),] + else: + return {} + for key,lbl,fmt in keylist: + if fmt is None: + indexDict[hist][lbl] = { + 'init' : (insVal[key],0), + 'val' : ('Instrument Parameters',0,key,1), + 'ref' : ('Instrument Parameters',0,key,2),} + # for instrument parameters assume prmname is based on key + indexDict[hist][lbl]['prmname'] = prmPrefix+key + else: + indexDict[hist][lbl] = { + 'str' : ('Instrument Parameters',0,key,1), + 'fmt' : fmt + } + return indexDict + +def getLimitVals(G2frame,Histograms): + '''Generate the Limits Data Table (a dict of dicts) with + all limits values for all histograms in the + selected histogram group (from G2frame.GroupInfo['groupName']). + This will be used to generate the contents of the GUI for limits values. + ''' + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupName = G2frame.GroupInfo['groupName'] + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + if groupName not in groupDict: + print(f'Unexpected: {groupName} not in groupDict') + return + # parameters to include in table + indexDict = {'_dataSource':Histograms} + # loop over histograms in group + for hist in groupDict[groupName]: + indexDict[hist] = {} + for lbl,indx in [('Tmin',0),('Tmax',1)]: + indexDict[hist][lbl] = { + 'val' : ('Limits',1,indx), + 'range': [Histograms[hist]['Limits'][0][0], + Histograms[hist]['Limits'][0][1]] + } + for i,item in enumerate(Histograms[hist]['Limits'][2:]): + for l,indx in [('Low',0),('High',1)]: + lbl = f'excl {l} {i+1}' + indexDict[hist][lbl] = { + 'val' : ('Limits',2+i,indx), + 'range': [Histograms[hist]['Limits'][0][0], + Histograms[hist]['Limits'][0][1]]} + return indexDict + +def getBkgVals(G2frame,Histograms): + '''Generate the Background Data Table (a dict of dicts) with + all Background values for all histograms in the + selected histogram group (from G2frame.GroupInfo['groupName']). + This will be used to generate the contents of the GUI for + Background values. + ''' + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupName = G2frame.GroupInfo['groupName'] + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + if groupName not in groupDict: + print(f'Unexpected: {groupName} not in groupDict') + return + # parameters to include in table + indexDict = {'_dataSource':Histograms} + # loop over histograms in group + for hist in groupDict[groupName]: + prmPrefix = f':{Histograms[hist]['hId']}:' + indexDict[hist] = {} + for lbl,indx,typ in [('Function type',0,'str'), + ('bkg refine flag',1,'ref'), + ('# Bkg terms',2,'val')]: + indexDict[hist][lbl] = { + typ : ('Background',0,indx) + } + if indx == 2: + indexDict[hist][lbl]['fmt'] = '.0f' + def OnChangeBkgTerms(hist=hist,Histograms=Histograms): + 'set the number of terms to match the new number' + nterms = Histograms[hist]['Background'][0][2] + Histograms[hist]['Background'][0][3:] = ( + Histograms[hist]['Background'][0][3:] + + 36*[0.0])[:nterms] + indexDict[hist][lbl]['setintfunc'] = OnChangeBkgTerms + indexDict[hist]['# Debye terms'] = { + 'str' : ('Background',1,'nDebye'), + 'fmt' : '.0f'} + for i,term in enumerate(Histograms[hist]['Background'][1]['debyeTerms']): + for indx,l in enumerate(['A','R','U']): + lbl = f'{l} #{i+1}' + indexDict[hist][lbl] = { + 'val' : ('Background',1,'debyeTerms',i,2*indx), + 'ref' : ('Background',1,'debyeTerms',i,2*indx+1), + 'prmname': f'{prmPrefix}Debye{l};{i}'} + indexDict[hist]['# Bkg Peaks'] = { + 'str' : ('Background',1,'nPeaks'), + 'fmt' : '.0f'} + for i,term in enumerate(Histograms[hist]['Background'][1]['peaksList']): + for indx,l in enumerate(['pos','int','sig','gam']): + lbl = f'{l} #{i+1}' + indexDict[hist][lbl] = { + 'val' : ('Background',1,'peaksList',i,2*indx), + 'ref' : ('Background',1,'peaksList',i,2*indx+1), + 'prmname': f'{prmPrefix}BkPk{l};{i}'} + if Histograms[hist]['Background'][1]['background PWDR'][0]: + val = 'yes' + else: + val = 'no' + indexDict[hist]['Fixed bkg file'] = { + 'str' : ('Background',1,'background PWDR',0), + 'txt' : val} + if Histograms[hist]['Background'][1]['background PWDR'][0]: + indexDict[hist]['Fixed bkg mult'] = { + 'ref' : ('Background',1,'background PWDR',2), + 'val' : ('Background',1,'background PWDR',1), + 'prmname': f'{prmPrefix}BF mult'} + return indexDict + +def getHAPvals(G2frame,phase,Histograms,Phases): + '''Generate the Parameter Data Table (a dict of dicts) with + all HAP values for the selected phase and all histograms in the + selected histogram group (from G2frame.GroupInfo['groupName']). + This will be used to generate the contents of the GUI for HAP values. + ''' + PhaseData = Phases[phase] + pId = Phases[phase]['pId'] + SGData = PhaseData['General']['SGData'] + cell = PhaseData['General']['Cell'][1:] + Amat,Bmat = G2lat.cell2AB(cell[:6]) + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + groupName = G2frame.GroupInfo['groupName'] + if groupName not in groupDict: + print(f'Unexpected: {groupName} not in groupDict') + return + indexDict = {'_dataSource':PhaseData['Histograms']} + for hist in groupDict[groupName]: + prmPrefix = f'{pId}:{Histograms[hist]['hId']}:' + indexDict[hist] = {} + # phase fraction + indexDict[hist]['Phase frac'] = { + 'val' : ('Scale',0), + 'ref' : ('Scale',1), + 'prmname': prmPrefix+'Scale', + } + PhaseData['Histograms'][hist]['LeBail'] = PhaseData['Histograms'][hist].get('LeBail',False) + indexDict[hist]['LeBail extract'] = { + 'str' : ('LeBail',), + 'txt' : "Yes" if PhaseData['Histograms'][hist]['LeBail'] else '(off)'} + # size values + if PhaseData['Histograms'][hist]['Size'][0] == 'isotropic': + indexDict[hist]['Size'] = { + 'val' : ('Size',1,0), + 'ref' : ('Size',2,0), + 'prmname': prmPrefix+'Size;i', + } + elif PhaseData['Histograms'][hist]['Size'][0] == 'uniaxial': + indexDict[hist]['Size/Eq'] = { + 'val' : ('Size',1,0), + 'ref' : ('Size',2,0), + 'prmname': prmPrefix+'Size;i',} + indexDict[hist]['Size/Ax'] = { + 'val' : ('Size',1,1), + 'ref' : ('Size',2,1), + 'prmname': prmPrefix+'Size;a',} + indexDict[hist]['Size/dir'] = { + 'str' : ('Size',3), + 'txt' : ','.join([str(i) for i in PhaseData['Histograms'][hist]['Size'][3]])} + else: + for i,lbl in enumerate(['S11','S22','S33','S12','S13','S23']): + indexDict[hist][f'Size/{lbl}'] = { + 'val' : ('Size',4,i), + 'ref' : ('Size',5,i), + 'prmname': prmPrefix+f'Size;{i}',} + indexDict[hist]['Size LGmix'] = { + 'val' : ('Size',1,2), + 'ref' : ('Size',2,2), + 'prmname': prmPrefix+'Size;mx',} + # microstrain values + if PhaseData['Histograms'][hist]['Mustrain'][0] == 'isotropic': + indexDict[hist]['\u00B5Strain'] = { + 'val' : ('Mustrain',1,0), + 'ref' : ('Mustrain',2,0), + 'prmname': prmPrefix+'Mustrain;i',} + elif PhaseData['Histograms'][hist]['Mustrain'][0] == 'uniaxial': + indexDict[hist]['\u00B5Strain/Eq'] = { + 'val' : ('Mustrain',1,0), + 'ref' : ('Mustrain',2,0), + 'prmname': prmPrefix+'Mustrain;i',} + indexDict[hist]['\u00B5Strain/Ax'] = { + 'val' : ('Mustrain',1,1), + 'ref' : ('Mustrain',2,1), + 'prmname': prmPrefix+'Mustrain;a',} + indexDict[hist]['\u00B5Strain/dir'] = { + 'str' : ('Mustrain',3), + 'txt' : ','.join([str(i) for i in PhaseData['Histograms'][hist]['Mustrain'][3]])} + else: + Snames = G2spc.MustrainNames(SGData) + for i,lbl in enumerate(Snames): + if i >= len(PhaseData['Histograms'][hist]['Mustrain'][4]): break + indexDict[hist][f'\u00B5Strain/{lbl}'] = { + 'val' : ('Mustrain',4,i), + 'ref' : ('Mustrain',5,i), + 'prmname': prmPrefix+f'Mustrain;{i}',} + muMean = G2spc.MuShklMean(SGData,Amat,PhaseData['Histograms'][hist]['Mustrain'][4][:len(Snames)]) + indexDict[hist]['\u00B5Strain/mean'] = { + 'str' : None, + 'txt' : f'{muMean:.2f}'} + indexDict[hist]['\u00B5Strain LGmix'] = { + 'val' : ('Mustrain',1,2), + 'ref' : ('Mustrain',2,2), + 'prmname': prmPrefix+'Mustrain;mx',} + + # Hydrostatic terms + Hsnames = G2spc.HStrainNames(SGData) + for i,lbl in enumerate(Hsnames): + if i >= len(PhaseData['Histograms'][hist]['HStrain'][0]): break + indexDict[hist][f'Recip Cell/{lbl}'] = { + 'val' : ('HStrain',0,i), + 'ref' : ('HStrain',1,i), + 'prmname': prmPrefix+f'{lbl}',} + # Preferred orientation terms + if PhaseData['Histograms'][hist]['Pref.Ori.'][0] == 'MD': + indexDict[hist]['March-Dollase'] = { + 'val' : ('Pref.Ori.',1), + 'ref' : ('Pref.Ori.',2), + 'prmname': prmPrefix+'MD',} + indexDict[hist]['M-D/dir'] = { + 'str' : ('Pref.Ori.',3), + 'txt' : ','.join([str(i) for i in PhaseData['Histograms'][hist]['Pref.Ori.'][3]])} + else: + indexDict[hist]['Spherical harmonics'] = { + 'ref' : ('Pref.Ori.',2),} + indexDict[hist]['SH order'] = { + 'str' : ('Pref.Ori.',4), + 'fmt' : '.0f'} + for lbl in PhaseData['Histograms'][hist]['Pref.Ori.'][5]: + indexDict[hist][f'SP {lbl}']= { + 'val' : ('Pref.Ori.',5,lbl), + 'prmname': prmPrefix+f'{lbl}',} + indexDict[hist]['SH txtr indx'] = { + 'str' : None, + 'txt' : f'{G2lat.textureIndex(PhaseData["Histograms"][hist]["Pref.Ori."][5]):.3f}'} + # misc: Layer Disp, Extinction + if 'Layer Disp' in PhaseData['Histograms'][hist]: + indexDict[hist]['Layer displ'] = { + 'val' : ('Layer Disp',0), + 'ref' : ('Layer Disp',1), + 'prmname': prmPrefix+'LayerDisp'} + if 'Extinction' in PhaseData['Histograms'][hist]: + indexDict[hist]['Extinction'] = { + 'val' : ('Extinction',0), + 'ref' : ('Extinction',1), + 'prmname': prmPrefix+'Extinction',} + if 'Babinet' in PhaseData['Histograms'][hist]: + indexDict[hist]['Babinet A'] = { + 'val' : ('Babinet','BabA',0), + 'ref' : ('Babinet','BabA',1), + 'prmname': prmPrefix+'BabA',} + if 'Babinet' in PhaseData['Histograms'][hist]: + indexDict[hist]['Babinet U'] = { + 'val' : ('Babinet','BabU',0), + 'ref' : ('Babinet','BabU',1), + 'prmname': prmPrefix+'BabU',} + return indexDict + +def computeShiftInfo(G2frame): + '''Define the contents of the array used to color + ValidatedTextCtrls by shift/sigma + ''' + covdata = G2frame.GPXtree.GetItemPyData( + G2gd.GetGPXtreeItemId(G2frame, + G2frame.root,'Covariance')) + for key in 'varyList','sig','Lastshft': # has a refinement been run? + if key not in covdata: return + shiftOsig = {} + if len(covdata['varyList']) == len(covdata['sig']) == len(covdata['Lastshft']): + for var,sig,shift in zip(covdata['varyList'],covdata['sig'],covdata['Lastshft']): + try: + shiftOsig[var] = float(shift/sig) + except: + pass + else: + print('Warning: in Covariance, varyList, sig and Lastshft do not have same lengths!') + vmin = None + vmax = 1.0 # shifts less than 1 are small + for v in shiftOsig.values(): + v = abs(v) + if vmin is None: + vmin = v + if vmax is None: + vmax = v + vmin = min(vmin,v) + vmax = max(vmax,v) + shiftLinearLevels = [] + shiftLogLevels = [] + shiftColors = [] + if shiftOsig: # come up with levels and colors + shiftSteps = 5 + for i in range(shiftSteps): + shiftLinearLevels.append(vmin+i*(vmax-vmin)/shiftSteps) + lmin = math.log(max(vmin,1e-8)) + lmax = math.log(max(vmax,1e-8)) + for i in range(shiftSteps): + shiftLogLevels.append(math.exp(lmin + i*(lmax-lmin)/shiftSteps)) + for i in range(shiftSteps): + #g = int(255.5-i*255/(shiftSteps-1)) + #shiftColors.append((255,g,0,255)) # yellow to red + #shiftColors.append((255-g,g,0,255)) # green to red + # generate light pink to dark pink + g = int(230.5-i*130/(shiftSteps-1)) + shiftColors.append((255,g,g,255)) # white to red + shiftInfo['shiftLinearLevels'] = shiftLinearLevels + shiftInfo['shiftLogLevels'] = shiftLogLevels + shiftInfo['shiftColors'] = shiftColors + shiftInfo['colorCaption'] = colorCaption + shiftInfo['shiftOsig'] = shiftOsig + +def colorCaption(G2frame): + 'Create legend info showing the range of shifts' + colorSizer = G2frame.dataWindow.bottomBox + colorSizer.Clear(True) + cp = G2frame.dataWindow.bottomPanel + if shiftInfo['colorMode'] == 'log': + levels = shiftInfo['shiftLogLevels'] + elif shiftInfo['colorMode'] == 'linear': + levels = shiftInfo['shiftLinearLevels'] + else: + return None + colorSizer.Add(wx.StaticText(cp,label='Shift/\u03c3 by color:'),0,WACV) + for i,(level,color) in enumerate(zip(levels,shiftInfo['shiftColors'])): + colorSizer.Add((15,-1)) + #b = wx.StaticText(cp,label=f'{i}: >{level:.4g}') + b = wx.StaticText(cp,label=f'>{level:.4g}') + b.SetBackgroundColour(color) + colorSizer.Add(b,0,WACV) + +def ColorTxtbox(txtctrl,DataArray,row): + '''Color a ValidatedTextCtrl textbox based on shift-over-sigma values + ''' + if shiftInfo['colorMode'] == 'log': + levels = shiftInfo['shiftLogLevels'] + elif shiftInfo['colorMode'] == 'linear': + levels = shiftInfo['shiftLinearLevels'] + else: + return + if 'prmname' in DataArray: + prmname = DataArray['prmname'] + if prmname in shiftInfo['shiftOsig']: + val = abs(shiftInfo['shiftOsig'][prmname]) + for (level,color) in reversed(list( + zip(levels,shiftInfo['shiftColors']))): + if val >= level: + txtctrl.SaveBackgroundColor(color) + break + else: # unexpected + txtctrl.SaveBackgroundColor(color) diff --git a/GSASII/GSASIImapvars.py b/GSASII/GSASIImapvars.py index dad32549..84711ed4 100644 --- a/GSASII/GSASIImapvars.py +++ b/GSASII/GSASIImapvars.py @@ -201,7 +201,8 @@ def GroupConstraints(constrDict): return groups,ParmList def GenerateConstraints(varyList,constrDict,fixedList,parmDict=None, - seqHistNum=None,raiseException=False): + seqHistNum=None,raiseException=False, + grEquivTbl=None,grHIDlist=None): '''Takes a list of relationship entries that have been stored by :func:`ProcessConstraints` into lists ``constrDict`` and ``fixedList`` @@ -242,6 +243,13 @@ def GenerateConstraints(varyList,constrDict,fixedList,parmDict=None, :param bool raiseException: When True, generation of an error causes an exception to be raised (used in sequential fits) + :param dict grEquivTbl: for grouped sequential refinements: converts + histogram numbers (hId) that are in other histogram + groups to one in the current group + + :param list grHIDlist: for grouped sequential refinements: is a list of + hId values for the current group of histograms + :returns: errmsg,warning,groups,parmlist **errmsg** @@ -307,8 +315,8 @@ def showChanged(group): constrVarList += [i for i in cdict if i not in constrVarList and not i.startswith('_')] # Process the equivalences; If there are conflicting parameters, move them into constraints - warning = CheckEquivalences(constrDict,varyList,fixedList,parmDict,seqHistNum=seqHistNum) - + warning = CheckEquivalences(constrDict,varyList,fixedList,parmDict,seqHistNum=seqHistNum, + grEquivTbl=grEquivTbl,grHIDlist=grHIDlist) # look through "Constr" and "New Var" constraints looking for zero multipliers and # Hold, Unvaried & Undefined parameters @@ -346,7 +354,7 @@ def showChanged(group): elif parmDict is not None and var not in parmDict: # not defined, constraint will not be used if var not in undefinedVars: undefinedVars.append(var) notDefList.append(var) - if seqHistNum is None: + if seqHistNum is None and grHIDlist is None: if ':dAx:' in var or ':dAy:' in var or ':dAz:' in var: # coordinates from undefined atoms if fixVal is None: problem = True # invalid in New Var @@ -362,7 +370,7 @@ def showChanged(group): dropList.append(var) else: valid += 1 - if seqHistNum is not None and len(notDefList) > 0 and valid == 0: + if (seqHistNum is not None or grHIDlist is not None) and len(notDefList) > 0 and valid == 0: # for sequential ref can quietly ignore constraints with all undefined vars notDefList = [] @@ -384,7 +392,7 @@ def showChanged(group): msg += v warn(msg,cdict,fixVal) if valid == 0: # no valid entries - if seqHistNum is None: + if seqHistNum is None and grHIDlist is None: warn('Ignoring this constraint; contains no refined parameters',cdict,prefix='\nUnused ') skipList.append(cnum) elif problem: # mix of valid & refined and undefined items, cannot use this @@ -589,7 +597,8 @@ def showChanged(group): # print(60*'=') return errmsg,warning,groups,parmlist # saved for sequential fits -def CheckEquivalences(constrDict,varyList,fixedList,parmDict=None,seqHistNum=None): +def CheckEquivalences(constrDict,varyList,fixedList,parmDict=None,seqHistNum=None, + grEquivTbl=None,grHIDlist=None): '''Process equivalence constraints, looking for conflicts such as where a parameter is used in both an equivalence and a constraint expression or where chaining is done (A->B and B->C). @@ -607,6 +616,11 @@ def CheckEquivalences(constrDict,varyList,fixedList,parmDict=None,seqHistNum=Non equivalences where a parameter is has been removed from a refinement. :param int seqHistNum: the hId number of the current histogram in a sequential fit. None (default) otherwise. + :param dict grEquivTbl: for grouped sequential refinements: converts + histogram numbers (hId) that are in other histogram + groups to one in the current group + :param list grHIDlist: for grouped sequential refinements: is a list of + hId values for the current group of histograms :returns: warning messages about changes that need to be made to equivalences ''' @@ -711,7 +725,7 @@ def warnEqv(msg,cnum=None,prefix='Problem with equivalence'): if cnum not in convertList: convertList.append(cnum) if msg: warnEqv('Converting to "Constr"',cnum) - if warninfo['msg'] and seqHistNum is not None: + if warninfo['msg'] and (seqHistNum is not None or grHIDlist is not None): # sequential fit -- print the recasts but don't stop with a warning print(warninfo['msg']) warninfo = {'msg':'', 'shown':-1} @@ -765,7 +779,7 @@ def warnEqv(msg,cnum=None,prefix='Problem with equivalence'): if i != 0: msg += ", " msg += var StoreHold(var,'Equiv fixed') - elif seqHistNum is not None: # don't need to warn for sequential fit + elif seqHistNum is not None or grHIDlist is not None: # don't need to warn for sequential fit removeList.append(cnum) continue else: @@ -859,7 +873,7 @@ def warnEqv(msg,cnum=None,prefix='Problem with equivalence'): del indParmList[cnum][j] return warninfo['msg'] -def ProcessConstraints(constList,seqmode='use-all',seqhst=None): +def ProcessConstraints(constList,seqmode='use-all',seqhst=None,grEquivTbl=None,grHIDlist=None): """Interpret the constraints in the constList input into a dictionary, etc. All :class:`GSASIIobj.G2VarObj` objects are mapped to the appropriate phase/hist/atoms based on the object internals (random Ids). If this can't be @@ -877,9 +891,15 @@ def ProcessConstraints(constList,seqmode='use-all',seqhst=None): When seqmode=='wildcards-only' then any constraint with a numerical histogram number is skipped. With seqmode=='auto-wildcard', any non-null constraint number is set to the selected histogram. - :param int seqhst: number for current histogram (used for - 'wildcards-only' or 'auto-wildcard' only). Should be None for - non-sequential fits. + :param int seqhst: defines a specific histogram that is used in a + (non-grouped) sequential refinement. None for a non-sequential or a + grouped sequential fit. Used only when seqmode is 'wildcards-only' or + 'auto-wildcard' only) + :param dict grEquivTbl: for grouped sequential refinements: converts + histogram numbers (hId) that are in other histogram + groups to one in the current group + :param list grHIDlist: for grouped sequential refinements: is a list of + hId values for the current group of histograms :returns: a tuple of (constrDict,fixedList,ignored) where: @@ -904,24 +924,67 @@ def ProcessConstraints(constList,seqmode='use-all',seqhst=None): for constr in constList: terms = copy.deepcopy(constr[:-3]) # don't change the tree contents # deal with wildcards in sequential fits - if seqmode == 'wildcards-only' and seqhst is not None: - skip = False - for term in terms: - if term[1].histogram == '*': - term[1] = term[1].varname(seqhst) - elif term[1].histogram: - skip = True - if skip: continue - elif seqmode == 'auto-wildcard' and seqhst is not None: - for term in terms: - term[1] = term[1].varname(seqhst) - elif seqhst is not None: - for term in terms: - if term[1].histogram == '*': + if seqhst is not None: + if seqmode == 'wildcards-only': + skip = False + for term in terms: + if term[1].histogram == '*': + term[1] = term[1].varname(seqhst) + elif term[1].histogram: + skip = True + if skip: continue + elif seqmode == 'auto-wildcard': + for term in terms: term[1] = term[1].varname(seqhst) + else: + for term in terms: + if term[1].histogram == '*': + term[1] = term[1].varname(seqhst) # else: # term[1] = term[1].varname() # does this change anything??? - # separate processing by constraint type + elif grHIDlist is not None: + varList = [] + multList = [] + wildcard = False + for term in terms: + if term[1].histogram == '*': + wildcard = True + break + if constr[-1] == 'e' and wildcard: # regardless of mode, equivs with * get expanded + for term in terms: # only expecting one term, but process all + if term[1].histogram == '*': + for ih in grHIDlist: + varList.append(term[1].varname(ih)) + multList.append(term[0]) + else: # unexpected, have a mix with & without wildcards, likely invalid but process is if valid + varList.append(term[1].varname()) + multList.append(term[0]) + if len(varList) == 1: continue # a single equivalence is meaningless, skip + firstvar = varList[0] + firstmult = multList[0] + eqlist = [] + for v,m in zip(varList[1:],multList[1:]): + eqlist.append([v,firstmult/m]) + StoreEquivalence(firstvar,eqlist,False) + continue + #elif wildcard: + # ignored += 1 # wildcards are only implemented in equivalences for grouped + # # refs. I don't think they make sense for any other constr. + # continue + if seqmode != 'wildcards-only' and not wildcard: # for 'auto-wildcard' & 'use-all', convert to current set of histograms + bad = False + for term in terms: + ih = term[1].hId() + if ih in grHIDlist: continue # already in current group, no change is needed + if ih not in grEquivTbl: # have a term with an unused histogram -- problem + bad = True + break + else: + term[1] = term[1].varname(grEquivTbl[ih]) + if bad: + ignored += 1 + continue + # separate processing by constraint type if constr[-1] == 'h': # process a hold var = str(terms[0][1]) diff --git a/GSASII/GSASIImiscGUI.py b/GSASII/GSASIImiscGUI.py index 02a62c16..ea71e880 100644 --- a/GSASII/GSASIImiscGUI.py +++ b/GSASII/GSASIImiscGUI.py @@ -8,8 +8,6 @@ ''' -from __future__ import division, print_function - # # Allow this to be imported without wx present. # try: # import wx @@ -559,6 +557,8 @@ def ProjFileOpen(G2frame,showProvenance=True): finally: dlg.Destroy() wx.BeginBusyCursor() + groupDict = {} + groupInserted = False # only need to do this once try: if GSASIIpath.GetConfigValue('show_gpxSize'): posPrev = 0 @@ -580,6 +580,14 @@ def ProjFileOpen(G2frame,showProvenance=True): #if unexpectedObject: # print(datum[0]) # GSASIIpath.IPyBreak() + # insert groups before any individual PDWR items + if datum[0].startswith('PWDR') and groupDict and not groupInserted: + Id = G2frame.GPXtree.AppendItem(parent=G2frame.root,text='Groups/Powder') + G2frame.GPXtree.SetItemPyData(Id,{}) + for nam in groupDict: + sub = G2frame.GPXtree.AppendItem(parent=Id,text=nam) + G2frame.GPXtree.SetItemPyData(sub,{}) + groupInserted = True Id = G2frame.GPXtree.AppendItem(parent=G2frame.root,text=datum[0]) if datum[0] == 'Phases' and GSASIIpath.GetConfigValue('SeparateHistPhaseTreeItem',False): G2frame.GPXtree.AppendItem(parent=G2frame.root,text='Hist/Phase') @@ -587,6 +595,8 @@ def ProjFileOpen(G2frame,showProvenance=True): for pdata in data[1:]: if pdata[0] in Phases: pdata[1].update(Phases[pdata[0]]) + elif datum[0] == 'Controls': + groupDict = datum[1].get('Groups',{}).get('groupDict',{}) elif updateFromSeq and datum[0] == 'Covariance': data[0][1] = CovData elif updateFromSeq and datum[0] == 'Rigid bodies': @@ -725,7 +735,7 @@ def ProjFileSave(G2frame): while item: data = [] name = G2frame.GPXtree.GetItemText(item) - if name.startswith('Hist/Phase'): # skip over this + if name.startswith('Hist/Phase') or name.startswith('Groups'): # skip over this item, cookie = G2frame.GPXtree.GetNextChild(G2frame.root, cookie) continue data.append([name,G2frame.GPXtree.GetItemPyData(item)]) diff --git a/GSASII/GSASIIobj.py b/GSASII/GSASIIobj.py index 4dc94197..7356749a 100644 --- a/GSASII/GSASIIobj.py +++ b/GSASII/GSASIIobj.py @@ -1041,6 +1041,9 @@ def __hash__(self): 'Allow G2VarObj to be a dict key by implementing hashing' return hash(self.varname()) + def hId(self): + return _lookup(HistRanIdLookup,self.histogram) + def varname(self,hist=None): '''Formats the GSAS-II variable name as a "traditional" GSAS-II variable string (p:h::a) or (p:h:) @@ -2022,6 +2025,21 @@ def SetupCalc(self,parmDict): # raise Exception,"No value for variable "+str(v) self.exprDict.update(self.fxnpkgdict) + # def UpdateCalc(self,updateD): + # '''Updates the expression evaluation after SetupCalc is called. + # This should be much faster than calling SetupCalc again + # + # Needs to be debugged and perhaps used to replace UpdateDict + # ''' + # for var,val in updateD.items(): + # if var in self.lblLookup: + # self.exprDict[self.lblLookup[var]] = val + # elif var in self.exprDict: + # self.exprDict[var] = val + # else: + # self.exprDict[var] = val + # print(f'unexpected var {var} in ExpressionCalc.UpdateCalc') + def UpdateVars(self,varList,valList): '''Update the dict for the expression with a set of values diff --git a/GSASII/GSASIIphsGUI2.py b/GSASII/GSASIIphsGUI2.py index 47c5a1bf..3309176d 100644 --- a/GSASII/GSASIIphsGUI2.py +++ b/GSASII/GSASIIphsGUI2.py @@ -1895,7 +1895,7 @@ def OnProj(event): # sanity check: should this project be fitting texture? mainSizer.Add(wx.StaticText(Texture,label= ' NB: Normally texture model fitting generally requires multiple datasets with differing sample orientations/detector values')) - if G2frame.testSeqRefineMode() and G2gd.GetGPXtreeItemId(G2frame,G2frame.root,'Sequential results'): + if bool(G2frame.testSeqRefineMode(True)) and G2gd.GetGPXtreeItemId(G2frame,G2frame.root,'Sequential results'): mainSizer.Add(wx.StaticText(Texture,label= " Sequential result found. Use Texture/Refine texture above. See Method B in texture tutorial.")) h,pd = G2frame.GetUsedHistogramsAndPhasesfromTree() diff --git a/GSASII/GSASIIplot.py b/GSASII/GSASIIplot.py index f548faa3..39bf7fa7 100644 --- a/GSASII/GSASIIplot.py +++ b/GSASII/GSASIIplot.py @@ -215,14 +215,14 @@ def __init__(self,parent,id=-1,dpi=None,**kwargs): class G2PlotMpl(_tabPlotWin): 'Creates a Matplotlib 2-D plot in the GSAS-II graphics window' - def __init__(self,parent,id=-1,dpi=None,publish=None,**kwargs): + def __init__(self,parent,id=-1,dpi=None,**kwargs): _tabPlotWin.__init__(self,parent,id=id,**kwargs) mpl.rcParams['legend.fontsize'] = 10 mpl.rcParams['axes.grid'] = False #TODO: set dpi here via config var: this changes the size of the labeling font 72-100 is normal self.figure = mplfig.Figure(dpi=dpi,figsize=(5,6)) self.canvas = Canvas(self,-1,self.figure) - self.toolbar = GSASIItoolbar(self.canvas,publish=publish) + self.toolbar = GSASIItoolbar(self.canvas) self.toolbar.Realize() self.plotStyle = {'qPlot':False,'dPlot':False,'sqrtPlot':False,'sqPlot':False, 'logPlot':False,'exclude':False,'partials':True,'chanPlot':False} @@ -317,6 +317,7 @@ def __init__(self,parent,id=-1,G2frame=None): self.allowZoomReset = True # this indicates plot should be updated not initialized # (BHT: should this be in tabbed panel rather than here?) self.lastRaisedPlotTab = None + self.savedPlotLims = None def OnNotebookKey(self,event): '''Called when a keystroke event gets picked up by the notebook window @@ -387,7 +388,7 @@ def GetTabIndex(self,label): # if plotNum is not None: # wx.CallAfter(self.SetSelectionNoRefresh,plotNum) - def FindPlotTab(self,label,Type,newImage=True,publish=None): + def FindPlotTab(self,label,Type,newImage=True,saveLimits=False): '''Open a plot tab for initial plotting, or raise the tab if it already exists Set a flag (Page.plotInvalid) that it has been redrawn Record the name of the this plot in self.lastRaisedPlotTab @@ -401,9 +402,8 @@ def FindPlotTab(self,label,Type,newImage=True,publish=None): :param bool newImage: forces creation of a new graph for matplotlib plots only (defaults as True) - :param function publish: reference to routine used to create a - publication version of the current mpl plot (default is None, - which prevents use of this). + :param bool saveLimits: When True, limits for all MPL axes (plots) + are saved in self.savedPlotLims. :returns: new,plotNum,Page,Plot,limits where * new: will be True if the tab was just created @@ -412,8 +412,9 @@ def FindPlotTab(self,label,Type,newImage=True,publish=None): the plot appears * Plot: the mpl.Axes object for the graphic (mpl) or the figure for openGL. - * limits: for mpl plots, when a plot already exists, this will be a tuple - with plot scaling. None otherwise. + * limits: for mpl plots, when a plot already exists, this + will be a tuple with plot scaling. None otherwise. Only appropriate + for plots with one set of axes. ''' limits = None Plot = None @@ -421,6 +422,7 @@ def FindPlotTab(self,label,Type,newImage=True,publish=None): new = False plotNum,Page = self.GetTabIndex(label) if Type == 'mpl' or Type == '3d': + if saveLimits: self.savePlotLims(Page) Axes = Page.figure.get_axes() Plot = Page.figure.gca() #get previous plot limits = [Plot.get_xlim(),Plot.get_ylim()] # save previous limits @@ -436,7 +438,7 @@ def FindPlotTab(self,label,Type,newImage=True,publish=None): except (ValueError,AttributeError): new = True if Type == 'mpl': - Plot = self.addMpl(label,publish=publish).gca() + Plot = self.addMpl(label).gca() elif Type == 'ogl': Plot = self.addOgl(label) elif Type == '3d': @@ -461,8 +463,63 @@ def FindPlotTab(self,label,Type,newImage=True,publish=None): Page.helpKey = self.G2frame.dataWindow.helpKey except AttributeError: Page.helpKey = 'HelpIntro' + try: + Page.toolbar.enableArrows() # Disable Arrow keys if present + except AttributeError: + pass return new,plotNum,Page,Plot,limits + def savePlotLims(self,Page=None,debug=False,label=None): + '''Make a copy of all the current axes in the notebook object + ''' + if label and Page: + print('Warning: label and Page defined in savePlotLims') + elif label: + try: + plotNum,Page = self.GetTabIndex(label) + except ValueError: + print(f'Warning: plot {label} not found in savePlotLims') + return + elif not Page: + print('Error: neither label nor Page defined in savePlotLims') + return + self.savedPlotLims = [ + [i.get_xlim() for i in Page.figure.get_axes()], + [i.get_ylim() for i in Page.figure.get_axes()]] + if debug: + print(f'saved {len(self.savedPlotLims[1])} axes limits') + #print( self.savedPlotLims) + def restoreSavedPlotLims(self,Page): + '''Restore the plot limits, when previously saved, and when + ``G2frame.restorePlotLimits`` is set to True, which + is done when ``GSASIIpwdplot.refPlotUpdate`` is called with + ``restore=True``, which indicates that "live plotting" is + finished. This is also set for certain plot key-press + combinations. + The restore operation can only be done once, as the limits + are deleted after use in this method. + ''' + if self.savedPlotLims is None: + #print('---- nothing to restore') + return + if not getattr(self.G2frame,'restorePlotLimits',False): + #print('---- restorePlotLimits not set') + return + savedPlotLims = self.savedPlotLims + axesList = Page.figure.get_axes() + if len(axesList) != len(savedPlotLims[0]): + #print('saved lengths differ',len(axesList),len(savedPlotLims[0])) + return + for i,ax in enumerate(axesList): + ax.set_xlim(savedPlotLims[0][i]) + ax.set_ylim(savedPlotLims[1][i]) + #print(i, + # savedPlotLims[0][i][0],savedPlotLims[0][i][1], + # savedPlotLims[1][i][0],savedPlotLims[1][i][1]) + self.savedPlotLims = None + self.G2frame.restorePlotLimits = False + Page.canvas.draw() + def _addPage(self,name,page): '''Add the newly created page to the notebook and associated lists. @@ -485,9 +542,9 @@ def _addPage(self,name,page): #page.replotKWargs = {} #self.skipPageChange = False - def addMpl(self,name="",publish=None): + def addMpl(self,name=""): 'Add a tabbed page with a matplotlib plot' - page = G2PlotMpl(self.nb,publish=publish) + page = G2PlotMpl(self.nb) self._addPage(name,page) return page.figure @@ -598,7 +655,7 @@ def InvokeTreeItem(self,pid): class GSASIItoolbar(Toolbar): 'Override the matplotlib toolbar so we can add more icons' - def __init__(self,plotCanvas,publish=None,Arrows=True): + def __init__(self,plotCanvas,Arrows=True): '''Adds additional icons to toolbar''' self.arrows = {} # try to remove a button from the bar @@ -623,16 +680,25 @@ def __init__(self,plotCanvas,publish=None,Arrows=True): prfx = 'Shift plot ' fil = ''.join([i[0].lower() for i in direc.split()]+['arrow.ico']) self.arrows[direc] = self.AddToolBarTool(sprfx+direc,prfx+direc,fil,self.OnArrow) - if publish: - self.AddToolBarTool('Publish plot','Create publishable version of plot','publish.ico',publish) + self.publishId = self.AddToolBarTool('Publish plot','Create publishable version of plot','publish.ico',self.Publish) + self.publishRoutine = None + self.EnableTool(self.publishId,False) self.Realize() + def setPublish(self,publish=None): + 'Set the routine to be used to publsh the plot' + self.publishRoutine = publish + self.EnableTool(self.publishId,bool(publish)) + def Publish(self,*args,**kwargs): + 'Called to publish the current plot' + if not self.publishRoutine: return + self.publishRoutine(*args,**kwargs) def set_message(self,s): ''' this removes spurious text messages from the tool bar ''' pass -# TODO: perhaps someday we could pull out the bitmaps and rescale there here +# TODO: perhaps someday we could pull out the bitmaps and rescale them here # def AddTool(self,*args,**kwargs): # print('AddTool',args,kwargs) # return Toolbar.AddTool(self,*args,**kwargs) @@ -656,6 +722,27 @@ def _update_view(self): wx.CallAfter(*self.updateActions) Toolbar._update_view(self) + def home(self, *args): + '''Override home button to clear saved GROUP plot limits and trigger replot. + This ensures that pressing home resets to full data range while retaining x-units. + For GROUP plots, we need to replot rather than use matplotlib's home because + matplotlib's home would restore the original shared limits, not per-histogram limits. + (based on MG/Cl Sonnet code) + ''' + G2frame = wx.GetApp().GetMainTopWindow() + # Check if we're in GROUP plot mode - if so, clear saved GROUP + # plot x-limits and trigger a replot + if self.arrows.get('_groupMode'): + # PlotPatterns will use full data range + if hasattr(G2frame, 'groupXlim'): + del G2frame.groupXlim + # Trigger a full replot for GROUP plots + if self.updateActions: + wx.CallAfter(*self.updateActions) + return + # For non-GROUP plots, call the parent's home method + Toolbar.home(self, *args) + def AnyActive(self): for Itool in range(self.GetToolsCount()): if self.GetToolState(self.GetToolByPos(Itool).GetId()): @@ -671,6 +758,22 @@ def GetActive(self): def OnArrow(self,event): 'reposition limits to scan or zoom by button press' + if self.arrows.get('_groupMode'): + Page = self.arrows['_groupMode'] + if event.Id == self.arrows['right']: + Page.groupOff += 1 + elif event.Id == self.arrows['left']: + Page.groupOff -= 1 + elif event.Id == self.arrows['Expand X']: + Page.groupMax += 1 + elif event.Id == self.arrows['Shrink X']: + if Page.groupMax == 2: return + Page.groupMax -= 1 + else: + return + if self.updateActions: + wx.CallLater(100,*self.updateActions) + return axlist = self.plotCanvas.figure.get_axes() if len(axlist) == 1: ax = axlist[0] @@ -728,6 +831,21 @@ def OnArrow(self,event): # self.parent.toolbar.push_current() if self.updateActions: wx.CallAfter(*self.updateActions) + def enableArrows(self,mode='',updateActions=None): + '''Disable/Enable arrow keys. + Disables when updateActions is None. + mode='group' turns on 'x' buttons only + ''' + if not self.arrows: return + self.updateActions = updateActions + if mode == 'group': + # assumes that all arrows previously disabled + for lbl in ('left', 'right', 'Expand X', 'Shrink X'): + self.EnableTool(self.arrows[lbl],True) + else: + for lbl in ('left','right','up','down', 'Expand X', + 'Shrink X','Expand Y','Shrink Y'): + self.EnableTool(self.arrows[lbl],bool(updateActions)) def OnHelp(self,event): 'Respond to press of help button on plot toolbar' diff --git a/GSASII/GSASIIpwdplot.py b/GSASII/GSASIIpwdplot.py index cdfb8bd7..3bf17926 100644 --- a/GSASII/GSASIIpwdplot.py +++ b/GSASII/GSASIIpwdplot.py @@ -83,6 +83,7 @@ plotOpt['lineWid'] = '1' plotOpt['saveCSV'] = False plotOpt['CSVfile'] = None +plotOpt['sharedX'] = False for xy in 'x','y': for minmax in 'min','max': key = f'{xy}{minmax}' @@ -97,6 +98,14 @@ def ReplotPattern(G2frame,newPlot,plotType,PatternName=None,PickName=None): to be plotted (pattern name, item picked in tree + eventually the reflection list) to be passed as names rather than references to wx tree items, defined as class entries ''' + if plotType == 'GROUP' and PickName: + gId = G2gd.GetGPXtreeItemId(G2frame, G2frame.root, 'Groups/Powder') + G2frame.PickId = G2gd.GetGPXtreeItemId(G2frame, gId, PickName) + G2frame.G2plotNB.savePlotLims(label='Powder Patterns') + G2frame.restorePlotLimits = True + if GSASIIpath.GetConfigValue('debug'): print('updating GROUP plot') + PlotPatterns(G2frame,plotType=plotType) + return if PatternName: pId = G2gd.GetGPXtreeItemId(G2frame, G2frame.root, PatternName) if pId: @@ -198,17 +207,19 @@ def OnPlotKeyPress(event): try: #one way to check if key stroke will work on plot Parms,Parms2 = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.PatternId, 'Instrument Parameters')) except TypeError: - G2frame.G2plotNB.status.SetStatusText('Select '+plottype+' pattern first',1) + G2frame.G2plotNB.status.SetStatusText(f'Select {plottype} pattern first',1) return + if 'GROUP' in plottype: # save plot limits in case we want to restore them + G2frame.G2plotNB.savePlotLims(Page) newPlot = False - if event.key == 'w': + if event.key == 'w' and not 'GROUP' in plottype: G2frame.Weight = not G2frame.Weight if not G2frame.Weight and not G2frame.Contour and 'PWDR' in plottype: G2frame.SinglePlot = True elif 'PWDR' in plottype: # Turning on Weight plot clears previous limits G2frame.FixedLimits['dylims'] = ['',''] - newPlot = True - elif event.key in ['shift+1','!']: # save current plot settings as defaults + #newPlot = True # this resets the x & y limits, not wanted! + elif event.key in ['shift+1','!'] and not 'GROUP' in plottype: # save current plot settings as defaults # shift+1 assumes US keyboard print('saving plotting defaults for',G2frame.GPXtree.GetItemText(G2frame.PatternId)) data = G2frame.GPXtree.GetItemPyData(G2frame.PatternId) @@ -222,9 +233,16 @@ def OnPlotKeyPress(event): G2frame.ErrorBars = not G2frame.ErrorBars elif event.key == 'T' and 'PWDR' in plottype: Page.plotStyle['title'] = not Page.plotStyle.get('title',True) - elif event.key == 'f' and 'PWDR' in plottype: # short,full length or no tick-marks + elif event.key == 'f' and ('PWDR' in plottype or 'GROUP' in plottype): # short,full length or no tick-marks if G2frame.Contour: return Page.plotStyle['flTicks'] = (Page.plotStyle.get('flTicks',0)+1)%3 + if 'GROUP' in plottype: G2frame.restorePlotLimits = True + elif event.key == 'x' and groupName is not None: # share X axis scale for Pattern Groups + plotOpt['sharedX'] = not plotOpt['sharedX'] + # Clear saved x-limits when toggling sharedX mode (MG/Cl Sonnet) + if hasattr(G2frame, 'groupXlim'): + del G2frame.groupXlim + newPlot = True elif event.key == 'x' and 'PWDR' in plottype: Page.plotStyle['exclude'] = not Page.plotStyle['exclude'] elif event.key == 'k' and G2frame.GPXtree.GetItemText(G2frame.PickId) == 'Background': @@ -242,9 +260,11 @@ def OnPlotKeyPress(event): elif event.key == '.': Page.plotStyle['WgtDiagnostic'] = not Page.plotStyle.get('WgtDiagnostic',False) newPlot = True - elif event.key == 'b' and plottype not in ['SASD','REFD'] and not Page.plotStyle['logPlot'] and not Page.plotStyle['sqrtPlot']: + elif event.key == 'b' and plottype not in ['SASD','REFD']: G2frame.SubBack = not G2frame.SubBack - elif event.key == 'n': + Page.plotStyle['sqrtPlot'] = False + Page.plotStyle['logPlot'] = False + elif event.key == 'n' and not 'GROUP' in plottype: if G2frame.Contour: pass else: @@ -252,9 +272,10 @@ def OnPlotKeyPress(event): if Page.plotStyle['logPlot']: Page.plotStyle['sqrtPlot'] = False else: + Page.plotStyle['Offset'] = Page.plotStyle.get('Offset',[0,0]) Page.plotStyle['Offset'][0] = 0 newPlot = True - elif event.key == 's' and 'PWDR' in plottype: + elif event.key == 's' and ('PWDR' in plottype or 'GROUP' in plottype): Page.plotStyle['sqrtPlot'] = not Page.plotStyle['sqrtPlot'] if Page.plotStyle['sqrtPlot']: Page.plotStyle['logPlot'] = False @@ -322,6 +343,7 @@ def OnPlotKeyPress(event): elif Page.plotStyle['Offset'][0] > -100.: Page.plotStyle['Offset'][0] -= 10. elif event.key == 'g': + if 'GROUP' in plottype: G2frame.restorePlotLimits = True mpl.rcParams['axes.grid'] = not mpl.rcParams['axes.grid'] elif event.key == 'l' and not G2frame.SinglePlot: Page.plotStyle['Offset'][1] -= 1. @@ -347,8 +369,8 @@ def OnPlotKeyPress(event): G2frame.Cmin = 0.0 Page.plotStyle['Offset'] = [0,0] elif event.key == 'C' and 'PWDR' in plottype and G2frame.Contour: - #G2G.makeContourSliders(G2frame,Ymax,PlotPatterns,newPlot,plotType) - G2G.makeContourSliders(G2frame,Ymax,PlotPatterns,True,plotType) # force newPlot=True, prevents blank plot on Mac + #G2G.makeContourSliders(G2frame,Ymax,PlotPatterns,newPlot,plottype) + G2G.makeContourSliders(G2frame,Ymax,PlotPatterns,True,plottype) # force newPlot=True, prevents blank plot on Mac elif event.key == 'c' and 'PWDR' in plottype: newPlot = True if not G2frame.Contour: @@ -362,8 +384,11 @@ def OnPlotKeyPress(event): Page.plotStyle['partials'] = not Page.plotStyle['partials'] elif (event.key == 'e' and 'PWDR' in plottype and G2frame.SinglePlot and ifLimits and not G2frame.Contour): + # set limits in response to the'e' key. First press sets one side + # in Page.startExclReg. Second 'e' press defines other side and + # causes region to be saved (after d/Q conversion if needed) Page.excludeMode = not Page.excludeMode - if Page.excludeMode: + if Page.excludeMode: # first key press try: # fails from key menu Page.startExclReg = event.xdata except AttributeError: @@ -376,17 +401,24 @@ def OnPlotKeyPress(event): axis.axvline(Page.startExclReg,color='b',dashes=(2,3)) Page.vLine = Plot.axvline(Page.startExclReg,color='b',dashes=(2,3)) SavePlot4animate() - else: + else: # second key press UnsavePlot4animate() - wx.CallAfter(PlotPatterns,G2frame,newPlot=False, - plotType=plottype,extraKeys=extraKeys) - if abs(Page.startExclReg - event.xdata) < 0.1: return LimitId = G2gd.GetGPXtreeItemId(G2frame,G2frame.PatternId, 'Limits') limdat = G2frame.GPXtree.GetItemPyData(LimitId) mn = min(Page.startExclReg, event.xdata) mx = max(Page.startExclReg, event.xdata) + if Page.plotStyle['qPlot']: + mn = G2lat.Dsp2pos(Parms,2.0*np.pi/mn) + mx = G2lat.Dsp2pos(Parms,2.0*np.pi/mx) + elif Page.plotStyle['dPlot']: + mn = G2lat.Dsp2pos(Parms,mn) + mx = G2lat.Dsp2pos(Parms,mx) + if mx < mn: mx,mn = mn,mx + #if abs(mx - mn) < 0.1: return # very small regions are ignored limdat.append([mn,mx]) G2pdG.UpdateLimitsGrid(G2frame,limdat,plottype) + wx.CallAfter(PlotPatterns,G2frame,newPlot=False, + plotType=plottype,extraKeys=extraKeys) return elif event.key == 'a' and 'PWDR' in plottype and G2frame.SinglePlot and not ( Page.plotStyle['logPlot'] or Page.plotStyle['sqrtPlot'] or G2frame.Contour): @@ -410,10 +442,17 @@ def OnPlotKeyPress(event): Pattern[0]['Magnification'] += [[xpos,2.]] wx.CallAfter(G2gd.UpdatePWHKPlot,G2frame,plottype,G2frame.PatternId) return - elif event.key == 'q' and not ifLimits: + elif event.key == 'q': newPlot = True - if 'PWDR' in plottype: + if 'PWDR' in plottype or plottype.startswith('GROUP'): Page.plotStyle['qPlot'] = not Page.plotStyle['qPlot'] + # switching from d to Q + if (Page.plotStyle['qPlot'] and + Page.plotStyle['dPlot'] and + getattr(G2frame, 'groupXlim', None) is not None): + G2frame.groupXlim = ( + 2.0 * np.pi / G2frame.groupXlim[1], # Q_max -> d_min + 2.0 * np.pi / G2frame.groupXlim[0]) # Q_min -> d_max Page.plotStyle['dPlot'] = False Page.plotStyle['chanPlot'] = False elif plottype in ['SASD','REFD']: @@ -426,9 +465,16 @@ def OnPlotKeyPress(event): elif event.key == 'e' and G2frame.Contour: newPlot = True G2frame.TforYaxis = not G2frame.TforYaxis - elif event.key == 't' and 'PWDR' in plottype and not ifLimits: - newPlot = True + elif event.key == 't' and ('PWDR' in plottype or plottype.startswith('GROUP')): + newPlot = True Page.plotStyle['dPlot'] = not Page.plotStyle['dPlot'] + # switching from Q to d + if (Page.plotStyle['qPlot'] and + Page.plotStyle['dPlot'] and + getattr(G2frame, 'groupXlim', None) is not None): + G2frame.groupXlim = ( + 2.0 * np.pi / G2frame.groupXlim[1], # Q_min <- d_max + 2.0 * np.pi / G2frame.groupXlim[0]) # Q_max <- d_min Page.plotStyle['qPlot'] = False Page.plotStyle['chanPlot'] = False elif event.key == 'm': @@ -438,7 +484,7 @@ def OnPlotKeyPress(event): G2frame.Contour = False newPlot = True elif event.key == 'F' and not G2frame.SinglePlot: - choices = G2gd.GetGPXtreeDataNames(G2frame,plotType) + choices = G2gd.GetGPXtreeDataNames(G2frame,plottype) dlg = G2G.G2MultiChoiceDialog(G2frame, 'Select dataset(s) to plot\n(select all or none to reset)', 'Multidata plot selection',choices) @@ -481,6 +527,11 @@ def OnPlotKeyPress(event): plotOpt['CSVfile'] = G2G.askSaveFile(G2frame,'','.csv', 'Comma separated variable file') if plotOpt['CSVfile']: plotOpt['saveCSV'] = True + elif event.key == 'k': # toggle use of crosshair cursor + if getattr(G2frame,'CrossHairs',False): + G2frame.CrossHairs = False + else: + G2frame.CrossHairs = True else: #print('no binding for key',event.key) return @@ -495,7 +546,7 @@ def OnMotion(event): global PlotList G2plt.SetCursor(Page) # excluded region animation - if Page.excludeMode: + if Page.excludeMode: # defining an excluded region if event.xdata is None or G2frame.GPXtree.GetItemText( G2frame.GPXtree.GetSelection()) != 'Limits': # reset if out of bounds or not on limits UnsavePlot4animate() @@ -645,7 +696,7 @@ def OnMotion(event): Page.SetToolTipString(s) except TypeError: - G2frame.G2plotNB.status.SetStatusText('Select '+plottype+' pattern first',1) + G2frame.G2plotNB.status.SetStatusText(f'Select {plottype} pattern first',1) def OnPress(event): np.seterr(invalid='ignore') #ugh - this removes a matplotlib error for mouse clicks in log plots @@ -718,9 +769,15 @@ def OnPickPwd(event): to create a peak or an excluded region ''' def OnDragMarker(event): - '''Respond to dragging of a plot Marker + '''Respond to dragging of a plot Marker (fixed background point) ''' - if event.xdata is None or event.ydata is None: return # ignore if cursor out of window + if event.xdata is None or event.ydata is None: # mouse is out of plot area, reset drag + G2frame.itemPicked = None + if G2frame.cid is not None: # delete drag connection + Page.canvas.mpl_disconnect(G2frame.cid) + G2frame.cid = None + wx.CallAfter(PlotPatterns,G2frame,plotType=plottype,extraKeys=extraKeys) + return if G2frame.itemPicked is None: return # not sure why this happens, if it does RestorePlot4animate() G2frame.itemPicked.set_data([event.xdata], [event.ydata]) @@ -734,12 +791,18 @@ def OnDragMarker(event): def OnDragLine(event): '''Respond to dragging of a plot line ''' - if event.xdata is None: return # ignore if cursor out of window + if event.xdata is None: # mouse is out of plot area, reset drag + G2frame.itemPicked = None + if G2frame.cid is not None: # delete drag connection + Page.canvas.mpl_disconnect(G2frame.cid) + G2frame.cid = None + wx.CallAfter(PlotPatterns,G2frame,plotType=plottype,extraKeys=extraKeys) + return if G2frame.itemPicked is None: return # not sure why this happens RestorePlot4animate() coords = G2frame.itemPicked.get_data() coords[0][0] = coords[0][1] = event.xdata - coords = G2frame.itemPicked.set_data(coords) + G2frame.itemPicked.set_data(coords) if G2frame.Weight: axis = Page.figure.axes[1] else: @@ -750,7 +813,13 @@ def OnDragLine(event): def OnDragLabel(event): '''Respond to dragging of a HKL label ''' - if event.xdata is None: return # ignore if cursor out of window + if event.ydata is None: # mouse is out of plot area, reset drag + G2frame.itemPicked = None + if G2frame.cid is not None: # delete drag connection + Page.canvas.mpl_disconnect(G2frame.cid) + G2frame.cid = None + wx.CallAfter(PlotPatterns,G2frame,plotType=plottype,extraKeys=extraKeys) + return if G2frame.itemPicked is None: return # not sure why this happens try: coords = list(G2frame.itemPicked.get_position()) @@ -776,7 +845,13 @@ def OnDragLabel(event): def OnDragTickmarks(event): '''Respond to dragging of the reflection tick marks ''' - if event.ydata is None: return # ignore if cursor out of window + if event.ydata is None: # mouse is out of plot area, reset drag + G2frame.itemPicked = None + if G2frame.cid is not None: # delete drag connection + Page.canvas.mpl_disconnect(G2frame.cid) + G2frame.cid = None + wx.CallAfter(PlotPatterns,G2frame,plotType=plottype,extraKeys=extraKeys) + return if Page.tickDict is None: return # not sure why this happens, if it does RestorePlot4animate() if Page.pickTicknum: @@ -800,7 +875,14 @@ def OnDragTickmarks(event): def OnDragDiffCurve(event): '''Respond to dragging of the difference curve. ''' - if event.ydata is None: return # ignore if cursor out of window + if event.ydata is None: # mouse is out of plot area, reset drag + G2frame.itemPicked = None + if G2frame.cid is not None: # delete drag connection + Page.canvas.mpl_disconnect(G2frame.cid) + G2frame.cid = None + wx.CallAfter(PlotPatterns,G2frame,plotType=plottype,extraKeys=extraKeys) + return + return # ignore if cursor out of window if G2frame.itemPicked is None: return # not sure why this happens RestorePlot4animate() coords = G2frame.itemPicked.get_data() @@ -836,6 +918,11 @@ def DeleteHKLlabel(HKLmarkers,key): if G2frame.itemPicked is not None: # only allow one selection return pick = event.artist + hist = pick.get_gid() + if hist: # is this a group histogram label? + if 'PWDR' in hist: + showHistogram(G2frame,hist,Page,Plot) + return xpos,ypos = pick.get_position() if event.mouseevent.button == 3: # right click, delete HKL label # but only 1st of the picked items, if multiple @@ -942,17 +1029,21 @@ def DeleteHKLlabel(HKLmarkers,key): if ind.all() != [0]: #picked a data point LimitId = G2gd.GetGPXtreeItemId(G2frame,G2frame.PatternId, 'Limits') limData = G2frame.GPXtree.GetItemPyData(LimitId) - # Q & d not currently allowed on limits plot - # if Page.plotStyle['qPlot']: #qplot - convert back to 2-theta - # xy[0] = G2lat.Dsp2pos(Parms,2*np.pi/xy[0]) - # elif Page.plotStyle['dPlot']: #dplot - convert back to 2-theta - # xy[0] = G2lat.Dsp2pos(Parms,xy[0]) + # reverse setting limits for Q plot & TOF + if Page.plotStyle['qPlot'] and 'T' in Parms['Type'][0]: + if G2frame.ifSetLimitsMode == 2: + G2frame.ifSetLimitsMode = 1 + elif G2frame.ifSetLimitsMode == 1: + G2frame.ifSetLimitsMode = 2 + # set limit selected limit or excluded region after menu command # point in pattern selected as new upper or lower limit - if G2frame.ifSetLimitsMode == 2: # set upper - limData[1][1] = max(xy[0],limData[1][0]) + if G2frame.ifSetLimitsMode == 2: + limData[1][1] = min(limData[0][1],max(xy[0],limData[0][0])) # upper elif G2frame.ifSetLimitsMode == 1: - limData[1][0] = min(xy[0],limData[1][1]) # set lower + limData[1][0] = max(limData[0][0],min(xy[0],limData[0][1])) # lower + if limData[1][0] > limData[1][1]: + limData[1][0],limData[1][1] = limData[1][1],limData[1][0] G2frame.ifSetLimitsMode = 0 G2frame.CancelSetLimitsMode.Enable(False) G2frame.GPXtree.SetItemPyData(LimitId,limData) @@ -1073,6 +1164,7 @@ def DeleteHKLlabel(HKLmarkers,key): Page.pickTicknum = Page.phaseList.index(pick) resetlist = [] for pId,phase in enumerate(Page.phaseList): # set the tickmarks to a lighter color + if phase not in Page.tickDict: return col = Page.tickDict[phase].get_color() rgb = mpcls.ColorConverter().to_rgb(col) rgb_light = [(2 + i)/3. for i in rgb] @@ -1466,12 +1558,19 @@ def onPlotFormat(event): def refPlotUpdate(Histograms,cycle=None,restore=False): '''called to update an existing plot during a Rietveld fit; it only updates the curves, not the reflection marks or the legend. - It should be called with restore=True to reset plotting - parameters after the refinement is done. + After the refinement is complete it is called with restore=True to + reset plotting parameters. + + Note that the ``Page.plotStyle`` values are stashed in + ``G2frame.savedPlotStyle`` to be restored after FindPlotTab is + called and ``G2frame.restorePlotLimits`` is set so that + saved plot limits will be applied when + ``G2frame.G2plotNB.restoreSavedPlotLims`` is called. ''' if restore: (G2frame.SinglePlot,G2frame.Contour,G2frame.Weight, - G2frame.plusPlot,G2frame.SubBack,Page.plotStyle['logPlot']) = savedSettings + G2frame.plusPlot,G2frame.SubBack,G2frame.savedPlotStyle) = savedSettings + G2frame.restorePlotLimits = True return if plottingItem not in Histograms: @@ -1665,6 +1764,90 @@ def UnsavePlot4animate(): a bitmap of the plot ''' savedPlot['box'] = savedPlot['map'] = None + def adjustDim(i,nx): + '''MPL creates a 1-D array when nx=1, 2-D otherwise. + This adjusts the array addressing. + ''' + if nx == 1: + return (0,1) + else: + return ((0,i),(1,i)) + + # Callback used to update y-limits when user zooms interactively (from MG/Cl Sonnet) + def onGroupXlimChanged(ax): + '''Callback to update y-limits for all panels in group plot when x-range changes. + We calculate the global y-range across all panels for the visible x-range, + then explicitly set y-limits on ALL panels. + + This code behaves a bit funny w/r to zoom or pan of one plot in a group + in that the plot that is modified can have its y-axis range changed, + but if the another plot is changed, then the previous plot is given the + same y-range as all the others, so only one y-range can be changed. + Perhaps this is because the zoom/pan is applied after the changes + here are applied. + + To do better, we probably need a mode that unlocks the coupling of + the y-axes ranges. + ''' + if getattr(G2frame,'stop_onGroupXlimChanged',False): + return # disable this routine when needed + xlim = ax.get_xlim() + # Save x-limits for persistence across refinements + if (plotOpt['sharedX'] and + (Page.plotStyle['qPlot'] or Page.plotStyle['dPlot'])): + G2frame.groupXlim = xlim + + # Calculate global y-range across ALL panels for visible x-range + global_ymin = float('inf') + global_ymax = float('-inf') + global_dzmin = float('inf') + global_dzmax = float('-inf') + max_tick_space = 0 + + for i in range(Page.groupN): + xarr = np.array(gX[i]) + xye = gdat[i] + mask = (xarr >= xlim[0]) & (xarr <= xlim[1]) + if np.any(mask): + # Calculate scaled y-values for visible data + scaleY = lambda Y, idx=i: (Y - gYmin[idx]) / (gYmax[idx] - gYmin[idx]) * 100 + visible_obs = scaleY(xye[1][mask]) + visible_calc = scaleY(xye[3][mask]) + visible_bkg = scaleY(xye[4][mask]) + ymin_visible = min(visible_obs.min(), visible_calc.min(), visible_bkg.min()) + ymax_visible = max(visible_obs.max(), visible_calc.max(), visible_bkg.max()) + global_ymin = min(global_ymin, ymin_visible) + global_ymax = max(global_ymax, ymax_visible) + # Track tick space needed + if not Page.plotStyle.get('flTicks', False): + max_tick_space = max(max_tick_space, len(RefTbl[i]) * 5) + else: + max_tick_space = max(max_tick_space, 1) + # Calculate diff y-limits + DZ_visible = (xye[1][mask] - xye[3][mask]) * np.sqrt(xye[2][mask]) + global_dzmin = min(global_dzmin, DZ_visible.min()) + global_dzmax = max(global_dzmax, DZ_visible.max()) + + # Apply global y-limits to ALL panels explicitly + if global_ymax > global_ymin: + yrange = global_ymax - global_ymin + ypad = max(yrange * 0.05, 1.0) + ylim_upper = (global_ymin - ypad - max_tick_space, global_ymax + ypad) + for i in range(Page.groupN): + up, down = adjustDim(i, Page.groupN) + Plots[up].set_ylim(ylim_upper) + Plots[up].autoscale(enable=False, axis='y') + if global_dzmax > global_dzmin: + dzrange = global_dzmax - global_dzmin + dzpad = max(dzrange * 0.05, 0.5) + ylim_lower = (global_dzmin - dzpad, global_dzmax + dzpad) + for i in range(Page.groupN): + up, down = adjustDim(i, Page.groupN) + Plots[down].set_ylim(ylim_lower) + Plots[down].autoscale(enable=False, axis='y') + # Force canvas redraw to apply new limits + Page.canvas.draw_idle() + #### beginning PlotPatterns execution ##################################### global exclLines,Page global DifLine @@ -1686,6 +1869,19 @@ def UnsavePlot4animate(): for i in 'Obs_color','Calc_color','Diff_color','Bkg_color': pwdrCol[i] = '#' + GSASIIpath.GetConfigValue(i,getDefault=True) + groupName = None + groupDict = {} + if plottype == 'GROUP': + groupName = G2frame.GroupInfo['groupName'] # set in GSASIIgroupGUI.UpdateGroup + Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + if groupName not in groupDict: + print(f'Unexpected: {groupName} not in groupDict') + return + # set data to first histogram in group + G2frame.PatternId = G2gd.GetGPXtreeItemId(G2frame, G2frame.root, groupDict[groupName][0]) + data = G2frame.GPXtree.GetItemPyData(G2frame.PatternId) + if not G2frame.PatternId: return if 'PKS' in plottype: # This is probably not used anymore; PlotPowderLines seems to be called directly @@ -1697,7 +1893,24 @@ def UnsavePlot4animate(): publish = PublishPlot else: publish = None - new,plotNum,Page,Plot,limits = G2frame.G2plotNB.FindPlotTab('Powder Patterns','mpl',publish=publish) + if G2frame.Contour: publish = None + + new,plotNum,Page,Plot,limits = G2frame.G2plotNB.FindPlotTab( + 'Powder Patterns','mpl',saveLimits=refineMode) + if hasattr(G2frame, 'savedPlotStyle'): + Page.plotStyle.update(G2frame.savedPlotStyle) + del G2frame.savedPlotStyle # do this only once & after Page is defined + if not getattr(G2frame,'CrossHairs',False): + if getattr(G2frame,'cursor',False): del G2frame.cursor + else: + G2frame.cursor = mpl.widgets.Cursor(Plot, useblit=True, color='red', linewidth=0.5) + Page.toolbar.setPublish(publish) + Page.toolbar.arrows['_groupMode'] = None + # if we are changing histogram types (including group to individual, reset plot) + if not new and hasattr(Page,'prevPlotType'): + if Page.prevPlotType != plottype: new = True + Page.prevPlotType = plottype + try: Selection = G2frame.GPXtree.GetItemText(G2frame.GPXtree.GetSelection()) except: @@ -1722,7 +1935,7 @@ def UnsavePlot4animate(): Page.excludeMode = False # True when defining an excluded region #patch - if 'Offset' not in Page.plotStyle and plotType in ['PWDR','SASD','REFD']: #plot offset data + if 'Offset' not in Page.plotStyle and plottype in ['PWDR','SASD','REFD']: #plot offset data Ymax = max(data[1][1]) Page.plotStyle.update({'Offset':[0.0,0.0],'delOffset':float(0.02*Ymax), 'refOffset':float(-0.1*Ymax),'refDelt':float(0.1*Ymax),}) @@ -1734,7 +1947,8 @@ def UnsavePlot4animate(): G2frame.lastPlotType except: G2frame.lastPlotType = None - if plotType == 'PWDR': + + if plottype == 'PWDR' or plottype == 'GROUP': try: Parms,Parms2 = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame, G2frame.PatternId, 'Instrument Parameters')) @@ -1774,7 +1988,8 @@ def UnsavePlot4animate(): plottingItem = G2frame.GPXtree.GetItemText(G2frame.PatternId) # save settings to be restored after refinement with repPlotUpdate({},restore=True) savedSettings = (G2frame.SinglePlot,G2frame.Contour,G2frame.Weight, - G2frame.plusPlot,G2frame.SubBack,Page.plotStyle['logPlot']) + G2frame.plusPlot,G2frame.SubBack, + copy.deepcopy(Page.plotStyle)) G2frame.SinglePlot = True G2frame.Contour = False G2frame.Weight = True @@ -1792,7 +2007,7 @@ def UnsavePlot4animate(): G2frame.PatternId = G2gd.GetGPXtreeItemId(G2frame, G2frame.root, plottingItem) data = G2frame.GPXtree.GetItemPyData(G2frame.PatternId) G2frame.GPXtree.SelectItem(G2frame.PatternId) - PlotPatterns(G2frame,True,plotType,None,extraKeys) + PlotPatterns(G2frame,True,plottype,None,extraKeys) #===================================================================================== elif 'PlotDefaults' in data[0] and fromTree: # set style from defaults saved with '!' #print('setting plot style defaults') @@ -1810,20 +2025,20 @@ def UnsavePlot4animate(): newPlot = True G2frame.Cmin = 0.0 G2frame.Cmax = 1.0 - Page.canvas.mpl_connect('motion_notify_event', OnMotion) - Page.canvas.mpl_connect('pick_event', OnPickPwd) - Page.canvas.mpl_connect('button_release_event', OnRelease) - Page.canvas.mpl_connect('button_press_event',OnPress) - Page.bindings = [] - # redo OnPlotKeyPress binding each time the Plot is updated - # since needs values that may have been changed after 1st call - try: - for b in Page.bindings: + # redo plot bindings each time the Plot is updated, since values + # may have been changed after 1st call + for b in G2frame.PlotBindings: + try: Page.canvas.mpl_disconnect(b) - except AttributeError: - pass - Page.bindings = [] - Page.bindings.append(Page.canvas.mpl_connect('key_press_event', OnPlotKeyPress)) + except AttributeError: + pass + G2frame.PlotBindings = [] + for e,r in [('motion_notify_event', OnMotion), + ('pick_event', OnPickPwd), + ('button_release_event', OnRelease), + ('button_press_event',OnPress), + ('key_press_event', OnPlotKeyPress)]: + G2frame.PlotBindings.append(Page.canvas.mpl_connect(e,r)) if not G2frame.PickId: print('No plot, G2frame.PickId,G2frame.PatternId=',G2frame.PickId,G2frame.PatternId) return @@ -1862,7 +2077,8 @@ def UnsavePlot4animate(): Phases = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.PatternId,'Reflection Lists')) Page.phaseList = sorted(Phases.keys()) # define an order for phases (once!) else: - Page.phaseList = Phases = [] + Histograms,Phases = G2frame.GetUsedHistogramsAndPhasesfromTree() + Page.phaseList = Phases # assemble a list of validated colors for tickmarks valid_colors = [] invalid_colors = [] @@ -1889,8 +2105,9 @@ def UnsavePlot4animate(): kwargs={'PatternName':G2frame.GPXtree.GetItemText(G2frame.PatternId)} if G2frame.PickId: kwargs['PickName'] = G2frame.GPXtree.GetItemText(G2frame.PickId) - wx.CallAfter(G2frame.G2plotNB.RegisterRedrawRoutine(G2frame.G2plotNB.lastRaisedPlotTab,ReplotPattern, - (G2frame,newPlot,plotType),kwargs)) + wx.CallAfter(G2frame.G2plotNB.RegisterRedrawRoutine( + G2frame.G2plotNB.lastRaisedPlotTab,ReplotPattern, + (G2frame,newPlot,plottype),kwargs)) except: #skip a C++ error pass # now start plotting @@ -1903,16 +2120,15 @@ def UnsavePlot4animate(): ifLimits = False if G2frame.GPXtree.GetItemText(G2frame.PickId) == 'Limits': ifLimits = True - Page.plotStyle['qPlot'] = False - Page.plotStyle['dPlot'] = False # Setup list of keypress keys in use for the current plot type - # a,b,c,d,e,f,g,i,l,m,n,o,p,q,r,s,t,u,w,x, (unused: j, k, y, z) + # a,b,c,d,e,f,g,i,k,l,m,n,o,p,q,r,s,t,u,w,x, (unused: j, y, z) # also: +,/, C,D,S,U if G2frame.Contour: Page.Choice = (' key press','b: toggle subtract background', 'd: lower contour max','u: raise contour max', 'D: lower contour min','U: raise contour min', 'o: reset contour limits','g: toggle grid', + 'k: toggle cross-hair cursor', 'i: interpolation method','S: color scheme','c: contour off', 'e: toggle temperature for y-axis','s: toggle sqrt plot', 'w: toggle w(Yo-Yc) contour plot','h: toggle channel # plot', @@ -1920,11 +2136,13 @@ def UnsavePlot4animate(): 'C: contour plot control window', ) else: +# Page.toolbar.updateActions = (PlotPatterns,G2frame) # command used to refresh after arrow key is pressed if 'PWDR' in plottype: Page.Choice = [' key press', 'a: add magnification region','b: toggle subtract background', 'c: contour on','x: toggle excluded regions','T: toggle plot title', 'f: toggle full-length ticks','g: toggle grid', + 'k: toggle cross-hair cursor', 'X: toggle cumulative chi^2', 'm: toggle multidata plot','n: toggle log(I)',] if plotOpt['obsInCaption']: @@ -1938,6 +2156,7 @@ def UnsavePlot4animate(): Page.Choice += [f'L: {addrem} {what} in legend',] if ifLimits: Page.Choice += ['e: create excluded region', + 'q: toggle Q plot','t: toggle d-spacing plot', 's: toggle sqrt plot','w: toggle (Io-Ic)/sig plot', '+: toggle obs line plot'] else: @@ -1968,6 +2187,7 @@ def UnsavePlot4animate(): elif plottype in ['SASD','REFD']: Page.Choice = [' key press', 'b: toggle subtract background file','g: toggle grid', + 'k: toggle cross-hair cursor', 'm: toggle multidata plot','n: toggle semilog/loglog', 'q: toggle S(q) plot','w: toggle (Io-Ic)/sig plot','+: toggle obs line plot',] if not G2frame.SinglePlot: @@ -1978,7 +2198,7 @@ def UnsavePlot4animate(): for KeyItem in extraKeys: Page.Choice = Page.Choice + [KeyItem[0] + ': '+KeyItem[2],] magLineList = [] # null value indicates no magnification - Page.toolbar.updateActions = None # no update actions + #Page.toolbar.updateActions = None # no update actions, used with the arrow keys G2frame.cid = None Page.keyPress = OnPlotKeyPress # assemble a list of validated colors (not currently needed) @@ -2026,7 +2246,7 @@ def UnsavePlot4animate(): else: #G2frame.selection Title = os.path.split(G2frame.GSASprojectfile)[1] if G2frame.selections is None: - choices = G2gd.GetGPXtreeDataNames(G2frame,plotType) + choices = G2gd.GetGPXtreeDataNames(G2frame,plottype) else: choices = G2frame.selections PlotList = [] @@ -2082,8 +2302,7 @@ def UnsavePlot4animate(): if Ymax is None: Ymax = max(xye[1]) Ymax = max(Ymax,max(xye[1])) if Ymax is None: return # nothing to plot - offsetX = Page.plotStyle['Offset'][1] - offsetY = Page.plotStyle['Offset'][0] + offsetY,offsetX = Page.plotStyle.get('Offset',(0,0))[:2] if Page.plotStyle['logPlot']: Title = 'log('+Title+')' elif Page.plotStyle['sqrtPlot']: @@ -2093,9 +2312,9 @@ def UnsavePlot4animate(): Title = 'Scaling diagnostic for '+Title if G2frame.SubBack: Title += ' - background' - if Page.plotStyle['qPlot'] or plottype in ['SASD','REFD'] and not G2frame.Contour and not ifLimits: + if Page.plotStyle['qPlot'] or plottype in ['SASD','REFD'] and not G2frame.Contour: xLabel = r'$Q, \AA^{-1}$' - elif Page.plotStyle['dPlot'] and 'PWDR' in plottype and not ifLimits: + elif Page.plotStyle['dPlot'] and ('PWDR' in plottype or plottype == 'GROUP'): xLabel = r'$d, \AA$' elif Page.plotStyle['chanPlot'] and G2frame.Contour: xLabel = 'Channel no.' @@ -2106,8 +2325,266 @@ def UnsavePlot4animate(): xLabel = 'E, keV' else: xLabel = r'$\mathsf{2\theta}$' + if groupName is not None: + # plot a group of histograms + Page.toolbar.arrows['_groupMode'] = Page # set up use of arrow keys + Page.toolbar.enableArrows('group',(PlotPatterns,G2frame,False,plotType,data)) - if G2frame.Weight and not G2frame.Contour: + Page.Choice = [' key press', + 'b: toggle subtract background', + 'f: toggle full-length ticks', + 'g: toggle grid', + 'k: toggle cross-hair cursor', + 's: toggle sqrt plot', # TODO: implement this + 'q: toggle Q plot', + 't: toggle d-spacing plot', + 'x: share x-axes (Q/d only)', + '+: toggle obs line plot'] + Plot.set_visible(False) # removes "big" plot + gXmin = {} + gXmax = {} + gYmin = {} + gYmax = {} + gX = {} + gdat = {} + totalrange = 0 + DZmax = 0 + DZmin = 0 + RefTbl = {} + histlbl = {} + # find portion of hist name that is the same and different + l = max([len(i) for i in groupDict[groupName]]) + h0 = groupDict[groupName][0].ljust(l) + msk = [True] * l + for h in groupDict[groupName][1:]: + msk = [m & (h0i == hi) for h0i,hi,m in zip(h0,h.ljust(l),msk)] + if not hasattr(Page,'groupMax'): Page.groupMax = min(10,len(groupDict[groupName])) + if not hasattr(Page,'groupOff'): Page.groupOff = 0 + groupPlotList = groupDict[groupName][Page.groupOff:] + groupPlotList += groupDict[groupName] # pad with more from beginning + groupPlotList = groupPlotList[:Page.groupMax] + Page.groupN = len(groupPlotList) + # place centered-dot in loc of non-common letters + #commonltrs = ''.join([h0i if m else '\u00B7' for (h0i,m) in zip(h0,msk)]) + # place rectangular box in the loc of non-common letter(s) + commonltrs = ''.join([h0i if m else '\u25A1' for (h0i,m) in zip(h0,msk)]) + pP = '+' + lW = 1.5 + if not G2frame.plusPlot: + pP = '' + lW = 1.5 + elif G2frame.plusPlot == 1: + pP = '+' + lW = 0 + elif G2frame.plusPlot == 2: # same as 0 for multiplot, TODO: rethink + pP = '' + lW = 1.5 + for i,h in enumerate(groupPlotList): + histlbl[i] = ''.join([hi for (hi,m) in zip(h,msk) if not m]) # unique letters + gPatternId = G2gd.GetGPXtreeItemId(G2frame, G2frame.root, h) + gParms,_ = G2frame.GPXtree.GetItemPyData( + G2gd.GetGPXtreeItemId(G2frame,gPatternId, + 'Instrument Parameters')) + LimitId = G2gd.GetGPXtreeItemId(G2frame,gPatternId, 'Limits') + limdat = G2frame.GPXtree.GetItemPyData(LimitId) + gd = G2frame.GPXtree.GetItemPyData(gPatternId) + RefTbl[i] = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,gPatternId,'Reflection Lists')) + # drop data outside limits + mask = (limdat[1][0] <= gd[1][0]) & (gd[1][0] <= limdat[1][1]) + gdat[i] = {} + for j in range(6): + gdat[i][j] = gd[1][j][mask] + # obs-calc/sigma + gdat[f'DZ{i}'] = (gdat[i][1]-gdat[i][3])*np.sqrt(gdat[i][2]) + DZmin = min(DZmin,gdat[f'DZ{i}'].min()) + DZmax = max(DZmax,gdat[f'DZ{i}'].max()) + if Page.plotStyle['sqrtPlot']: + for j in (1,3,4): + y = gdat[i][j] + gdat[i][j] = np.where(y>=0.,np.sqrt(y),-np.sqrt(-y)) + elif G2frame.SubBack: + gdat[i][3] = gdat[i][3] - gdat[i][4] + gdat[i][1] = gdat[i][1] - gdat[i][4] + if Page.plotStyle['qPlot']: + gX[i] = 2.*np.pi/G2lat.Pos2dsp(gParms,gdat[i][0]) + elif Page.plotStyle['dPlot']: + gX[i] = G2lat.Pos2dsp(gParms,gdat[i][0]) + else: + gX[i] = gdat[i][0] + gXmin[i] = min(gX[i]) + gXmax[i] = max(gX[i]) + # Calculate Y range from full data initially (may be updated later for zoom) + gYmax[i] = max(max(gdat[i][1]),max(gdat[i][3])) + gYmin[i] = min(min(gdat[i][1]),min(gdat[i][3])) + totalrange += gXmax[i]-gXmin[i] + if plotOpt['sharedX'] and ( + Page.plotStyle['qPlot'] or Page.plotStyle['dPlot']): + Page.figure.text(0.94,0.03,'X shared',fontsize=10, + color='g') + GS_kw = {'height_ratios':[4, 1]} + #Plots = Page.figure.subplots(2,Page.groupN,sharey='row',sharex=True, + # gridspec_kw=GS_kw) + # Don't use sharey='row' when sharedX - we'll manage y-limits manually + # This avoids conflicts between sharey and our dynamic y-limit updates + Plots = Page.figure.subplots(2,Page.groupN,sharex=True, + gridspec_kw=GS_kw) + else: + # apportion axes lengths making some plots bigger so that initially units are equal + xfrac = [(gXmax[i]-gXmin[i])/totalrange for i in range(Page.groupN)] + GS_kw = {'height_ratios':[4, 1], 'width_ratios':xfrac,} + Plots = Page.figure.subplots(2,Page.groupN,sharey='row',sharex='col', + gridspec_kw=GS_kw) + Page.figure.subplots_adjust(left=5/100.,bottom=16/150., + right=.99,top=1.-3/200.,hspace=0,wspace=0) + + # Connect callback for all panels when sharedX is enabled + up,down = adjustDim(0,Page.groupN) + if (plotOpt['sharedX'] and + (Page.plotStyle['qPlot'] or Page.plotStyle['dPlot'])): + Plots[up].callbacks.connect('xlim_changed', onGroupXlimChanged) + + # pretty up the tick labels + Plots[up].tick_params(axis='y', direction='inout', left=True, right=True) + Plots[down].tick_params(axis='y', direction='inout', left=True, right=True) + if Page.groupN > 1: + for ax in Plots[:,1:].ravel(): + ax.tick_params(axis='y', direction='in', left=True, right=True) + # remove 1st upper y-label so that it does not overlap with lower box + Plots[up].get_yticklabels()[0].set_visible(False) + Plots[down].set_ylabel(r'$\mathsf{\Delta I/\sigma_I}$',fontsize=12) + if Page.plotStyle['sqrtPlot']: + Plots[up].set_ylabel(r'$\rm\sqrt{Normalized\ intensity}$',fontsize=12) + elif G2frame.SubBack: + Plots[up].set_ylabel('Normalized Intensity-bkg',fontsize=12) + else: + Plots[up].set_ylabel('Normalized Intensity',fontsize=12) + Page.figure.text(0.001,0.03,commonltrs,fontsize=13,color='g') + Page.figure.supxlabel(xLabel) + for i,h in enumerate(groupPlotList): + up,down = adjustDim(i,Page.groupN) + Plot = Plots[up] + Plot1 = Plots[down] + pos = 0.02 + ha = 'left' + if Page.plotStyle['qPlot']: + pos = 0.95 + ha = 'right' + Plot.text(pos,0.98,histlbl[i], + transform=Plot.transAxes,gid=h, + verticalalignment='top', + horizontalalignment=ha, + fontsize=14,color='g',fontweight='bold',picker=4) + xye = gdat[i] + DifLine = Plot1.plot(gX[i],gdat[f'DZ{i}'],pwdrCol['Diff_color']) #,picker=1.,label=incCptn('diff')) #(Io-Ic)/sig(Io) + if G2frame.SubBack: + scaleY = lambda Y: Y/(gYmax[i]-gYmin[i])*100 + else: + scaleY = lambda Y: (Y-gYmin[i])/(gYmax[i]-gYmin[i])*100 + Plot.plot(gX[i],scaleY(xye[1]),marker=pP,color=pwdrCol['Obs_color'],linewidth=lW,# picker=3., + clip_on=Clip_on,label=incCptn('obs')) + Plot.plot(gX[i],scaleY(xye[3]),pwdrCol['Calc_color'],picker=0.,label=incCptn('calc'),linewidth=1.5) + if not G2frame.SubBack: + Plot.plot(gX[i],scaleY(xye[4]),pwdrCol['Bkg_color'],picker=0.,label=incCptn('bkg'),linewidth=1.5) #background + drawTicks(G2frame,RefTbl[i],Page,Plot,group=True) + + # Set axis limits AFTER plotting data to prevent autoscaling from overriding them (MG/Cl Sonnet) + # When sharedX is enabled, calculate common x-range encompassing all histograms + if (plotOpt['sharedX'] and + (Page.plotStyle['qPlot'] or Page.plotStyle['dPlot'])): + if getattr(G2frame, 'groupXlim', None) is not None: + commonXlim = getattr(G2frame, 'groupXlim', None) + else: + # Calculate union of all histogram x-ranges + commonXmin = min(gXmin.values()) + commonXmax = max(gXmax.values()) + commonXlim = (commonXmin, commonXmax) + + # First pass: set x-limits and calculate global y-range for visible data + # Since sharey='row', all upper panels share y-limits, all lower panels share y-limits + global_ymin = float('inf') + global_ymax = float('-inf') + global_dzmin = float('inf') + global_dzmax = float('-inf') + max_tick_space = 0 + + for i in range(Page.groupN): + up, down = adjustDim(i, Page.groupN) + if (plotOpt['sharedX'] and + (Page.plotStyle['qPlot'] or Page.plotStyle['dPlot'])): + xlim = commonXlim + Plots[up].set_xlim(xlim) + Plots[down].set_xlim(xlim) + else: + xlim = (gXmin[i], gXmax[i]) + Plots[up].set_xlim(xlim) + Plots[down].set_xlim(xlim) + + # Calculate y-range for this panel's visible data + xarr = np.array(gX[i]) + xye = gdat[i] + DZ = gdat[f'DZ{i}'] + mask = (xarr >= xlim[0]) & (xarr <= xlim[1]) + if np.any(mask): + scaleY = lambda Y, idx=i: (Y - gYmin[idx]) / (gYmax[idx] - gYmin[idx]) * 100 + visible_obs = scaleY(xye[1][mask]) + visible_calc = scaleY(xye[3][mask]) + visible_bkg = scaleY(xye[4][mask]) + ymin_visible = min(visible_obs.min(), visible_calc.min(), visible_bkg.min()) + ymax_visible = max(visible_obs.max(), visible_calc.max(), visible_bkg.max()) + global_ymin = min(global_ymin, ymin_visible) + global_ymax = max(global_ymax, ymax_visible) + # Track tick space needed + if not Page.plotStyle.get('flTicks', False): + max_tick_space = max(max_tick_space, len(RefTbl[i]) * 5) + else: + max_tick_space = max(max_tick_space, 1) + # Calculate diff y-limits + DZ_visible = DZ[mask] + global_dzmin = min(global_dzmin, DZ_visible.min()) + global_dzmax = max(global_dzmax, DZ_visible.max()) + + # Apply global y-limits to ALL panels explicitly (sharey may not propagate properly) + if global_ymax > global_ymin: + yrange = global_ymax - global_ymin + ypad = max(yrange * 0.05, 1.0) + ylim_upper = (global_ymin - ypad - max_tick_space, global_ymax + ypad) + else: + # Fallback to full range + if not Page.plotStyle.get('flTicks', False): + ylim_upper = (-max_tick_space, 102) + else: + ylim_upper = (-1, 102) + if global_dzmax > global_dzmin: + dzrange = global_dzmax - global_dzmin + dzpad = max(dzrange * 0.05, 0.5) + ylim_lower = (global_dzmin - dzpad, global_dzmax + dzpad) + else: # I do not undesrstand this + ylim_lower = (DZmin, DZmax) + + # Set y-limits on ALL panels + for i in range(Page.groupN): + up, down = adjustDim(i, Page.groupN) + Plots[up].set_ylim(ylim_upper) + Plots[down].set_ylim(ylim_lower) + + try: # try used as in PWDR menu not Groups + # Not sure if this does anything + G2frame.dataWindow.moveTickLoc.Enable(False) + G2frame.dataWindow.moveTickSpc.Enable(False) + # G2frame.dataWindow.moveDiffCurve.Enable(True) + except: + pass + # Save the current x-limits for GROUP plots so they can be restored after refinement (MG/Cl Sonnet) + # When sharedX is enabled in Q/d-space, all panels share the same x-range + if (plotOpt['sharedX'] and + (Page.plotStyle['qPlot'] or Page.plotStyle['dPlot'])): + up,down = adjustDim(0,Page.groupN) + G2frame.groupXlim = Plots[up].get_xlim() + wx.CallAfter(G2frame.G2plotNB.restoreSavedPlotLims,Page) # restore limits to previous, if saved & requested + Page.canvas.draw() + return # end of group plot + #========================================================================= + elif G2frame.Weight and not G2frame.Contour: Plot.set_visible(False) #hide old plot frame, will get replaced below GS_kw = {'height_ratios':[4, 1],} # try: @@ -2116,10 +2593,12 @@ def UnsavePlot4animate(): # Plot,Plot1 = MPLsubplots(Page.figure, 2, 1, sharex=True, gridspec_kw=GS_kw) Plot1.set_ylabel(r'$\mathsf{\Delta(I)/\sigma(I)}$',fontsize=16) Plot1.set_xlabel(xLabel,fontsize=16) - Page.figure.subplots_adjust(left=16/100.,bottom=16/150., - right=.98,top=1.-16/200.,hspace=0) else: Plot.set_xlabel(xLabel,fontsize=16) + Page.figure.subplots_adjust(left=8/100.,bottom=16/150., + right=.98,top=1.-8/100.,hspace=0,wspace=0) + if not G2frame.Contour: + Page.toolbar.enableArrows('',(PlotPatterns,G2frame)) if G2frame.Weight and G2frame.Contour: Title = r'$\mathsf{\Delta(I)/\sigma(I)}$ for '+Title if 'T' in ParmList[0]['Type'][0] or (Page.plotStyle['Normalize'] and not G2frame.SinglePlot): @@ -2169,9 +2648,9 @@ def UnsavePlot4animate(): if G2frame.Contour: # detect unequally spaced points in a contour plot for N,Pattern in enumerate(PlotList): xye = np.array(ma.getdata(Pattern[1])) # strips mask = X,Yo,W,Yc,Yb,Yd - if Page.plotStyle['qPlot'] and 'PWDR' in plottype and not ifLimits: + if Page.plotStyle['qPlot'] and 'PWDR' in plottype: X = 2.*np.pi/G2lat.Pos2dsp(Parms,xye[0]) - elif Page.plotStyle['dPlot'] and 'PWDR' in plottype and not ifLimits: + elif Page.plotStyle['dPlot'] and 'PWDR' in plottype: X = G2lat.Pos2dsp(Parms,xye[0]) else: X = copy.deepcopy(xye[0]) @@ -2288,7 +2767,6 @@ def UnsavePlot4animate(): lbl = Plot.annotate("x{}".format(ml0), xy=(tcorner, tpos), xycoords="axes fraction", verticalalignment='bottom',horizontalalignment=halign,label='_maglbl') Plot.magLbls.append(lbl) - Page.toolbar.updateActions = (PlotPatterns,G2frame) multArray = ma.getdata(multArray) if 'PWDR' in plottype: YI = copy.copy(xye[1]) #yo @@ -2319,9 +2797,9 @@ def UnsavePlot4animate(): if ifpicked and not G2frame.Contour: # draw limit & excluded region lines lims = limits[1:] - if Page.plotStyle['qPlot'] and 'PWDR' in plottype and not ifLimits: + if Page.plotStyle['qPlot'] and 'PWDR' in plottype: lims = 2.*np.pi/G2lat.Pos2dsp(Parms,lims) - elif Page.plotStyle['dPlot'] and 'PWDR' in plottype and not ifLimits: + elif Page.plotStyle['dPlot'] and 'PWDR' in plottype: lims = G2lat.Pos2dsp(Parms,lims) # limit lines Lines.append(Plot.axvline(lims[0][0],color='g',dashes=(5,5),picker=3.)) @@ -2526,12 +3004,12 @@ def UnsavePlot4animate(): else: Plot.plot(X,YB,color=pwdrCol['Obs_color'],marker=pP,linewidth=lW, picker=3.,clip_on=Clip_on,label=incCptn('obs')) - Plot.plot(X,ZB,pwdrCol['Bkg_color'],picker=False,label=incCptn('calc'),linewidth=1.5) + Plot.plot(X,ZB,pwdrCol['Bkg_color'],picker=0.,label=incCptn('calc'),linewidth=1.5) if 'PWDR' in plottype and (G2frame.SinglePlot and G2frame.plusPlot): - BackLine = Plot.plot(X,W/ymax,pwdrCol['Bkg_color'],picker=False,label=incCptn('bkg'),linewidth=1.5) #Ib + BackLine = Plot.plot(X,W/ymax,pwdrCol['Bkg_color'],picker=0.,label=incCptn('bkg'),linewidth=1.5) #Ib if not G2frame.Weight and np.any(Z): DifLine = Plot.plot(X,D/ymax,pwdrCol['Diff_color'],linewidth=1.5, - picker=True,pickradius=1.,label=incCptn('diff')) #Io-Ic + picker=1.,label=incCptn('diff')) #Io-Ic Plot.axhline(0.,color='k',label='_zero') Plot.tick_params(labelsize=14) @@ -2641,7 +3119,7 @@ def UnsavePlot4animate(): # waterfall mode=3: name in legend? name = Pattern[2] if Pattern[0].get('histTitle'): name = Pattern[0]['histTitle'] - Plot.plot(X,Y/ymax,color=mcolors.cmap(icolor),picker=False,label=incCptn(name)) + Plot.plot(X,Y/ymax,color=mcolors.cmap(icolor),picker=0.,label=incCptn(name)) elif plottype in ['SASD','REFD']: try: Plot.loglog(X,Y,mcolors.cmap(icolor),nonpositive='mask',linewidth=1.5) @@ -2691,7 +3169,7 @@ def UnsavePlot4animate(): if Page.plotStyle['sqrtPlot']: ypos = np.sqrt(abs(ypos))*np.sign(ypos) artist = Plot.text(xpos,ypos,lbl,fontsize=font,c=color,ha='center', - va='top',bbox=props,picker=True,rotation=angle,label='_'+ph) + va='top',bbox=props,picker=1.,rotation=angle,label='_'+ph) artist.key = (ph,key) #============================================================ if timeDebug: @@ -2763,42 +3241,7 @@ def UnsavePlot4animate(): or (inXtraPeakMode and G2frame.GPXtree.GetItemText(G2frame.PickId) == 'Peak List') ): - l = GSASIIpath.GetConfigValue('Tick_length',8.0) - w = GSASIIpath.GetConfigValue('Tick_width',1.) - for pId,phase in enumerate(Page.phaseList): - if 'list' in str(type(Phases[phase])): - continue - if phase in Page.phaseColors: - plcolor = Page.phaseColors[phase] - else: # how could this happen? - plcolor = 'k' - #continue - peaks = Phases[phase].get('RefList',[]) - if not len(peaks): - continue - if Phases[phase].get('Super',False): - peak = np.array([[peak[5],peak[6]] for peak in peaks]) - else: - peak = np.array([[peak[4],peak[5]] for peak in peaks]) - pos = Page.plotStyle['refOffset']-pId*Page.plotStyle['refDelt']*np.ones_like(peak) - if Page.plotStyle['qPlot']: - xtick = 2*np.pi/peak.T[0] - elif Page.plotStyle['dPlot']: - xtick = peak.T[0] - else: - xtick = peak.T[1] - if Page.plotStyle.get('flTicks',0) == 0: # short tick-marks - Page.tickDict[phase],_ = Plot.plot( - xtick,pos,'|',mew=w,ms=l,picker=3.,label=phase,color=plcolor) - # N.B. above creates two Line2D objects, 2nd is ignored. - # Not sure what each does. - elif Page.plotStyle.get('flTicks',0) == 1: # full length tick-marks - if len(xtick) > 0: - # create an ~hidden tickmark to create a legend entry - Page.tickDict[phase] = Plot.plot(xtick[0],0,'|',mew=0.5,ms=l, - label=phase,color=plcolor)[0] - for xt in xtick: # a separate line for each reflection position - Plot.axvline(xt,color=plcolor,picker=3.,label='_FLT_'+phase,lw=0.5) + drawTicks(G2frame,Phases,Page,Plot,Page.phaseList) handles,legends = Plot.get_legend_handles_labels() if handles: labels = dict(zip(legends,handles)) # remove duplicate phase entries @@ -2980,7 +3423,10 @@ def UnsavePlot4animate(): G2frame.dataWindow.moveTickSpc.Enable(True) if DifLine[0]: G2frame.dataWindow.moveDiffCurve.Enable(True) - if refineMode: return refPlotUpdate + if refineMode: + return refPlotUpdate + else: + wx.CallLater(100,G2frame.G2plotNB.restoreSavedPlotLims,Page) # restore limits to previous, if saved def PublishRietveldPlot(G2frame,Pattern,Plot,Page,reuse=None): '''Creates a window to show a customizable "Rietveld" plot. Exports that @@ -4497,3 +4943,223 @@ def StyleChange(*args): mainSizer.Fit(dlg) dlg.ShowModal() StyleChange() + +def plotVline(Page,Plot,Lines,Parms,pos,color,pickrad,style='dotted'): + '''shortcut to plot vertical lines for limits & Laue satellites. + Was used for extrapeaks''' + if not pickrad: pickrad = 0.0 + if Page.plotStyle['qPlot']: + Lines.append(Plot.axvline(2.*np.pi/G2lat.Pos2dsp(Parms,pos),color=color, + picker=pickrad,linestyle=style)) + elif Page.plotStyle['dPlot']: + Lines.append(Plot.axvline(G2lat.Pos2dsp(Parms,pos),color=color, + picker=pickrad,linestyle=style)) + else: + Lines.append(Plot.axvline(pos,color=color, + picker=pickrad,linestyle=style)) + +def drawTicks(G2frame,RefList,Page,MPLaxes,phaseList=None,group=False): + 'Draw the tickmarks for phases in the current histogram' + if phaseList is None: phaseList = list(RefList.keys()) + l = GSASIIpath.GetConfigValue('Tick_length',8.0) + w = GSASIIpath.GetConfigValue('Tick_width',1.) + for pId,phase in enumerate(phaseList): + if 'list' in str(type(RefList[phase])): + continue + if phase in Page.phaseColors: + plcolor = Page.phaseColors[phase] + else: # how could this happen? + plcolor = 'k' + #continue + peaks = RefList[phase].get('RefList',[]) + if not len(peaks): + continue + if RefList[phase].get('Super',False): + peak = np.array([[peak[5],peak[6]] for peak in peaks]) + else: + peak = np.array([[peak[4],peak[5]] for peak in peaks]) + if group: + pos = (2.5-len(phaseList)*5 + pId*5)**np.ones_like(peak) # tick positions hard-coded + else: + pos = Page.plotStyle['refOffset']-pId*Page.plotStyle['refDelt']*np.ones_like(peak) + if Page.plotStyle['qPlot']: + xtick = 2*np.pi/peak.T[0] + elif Page.plotStyle['dPlot']: + xtick = peak.T[0] + else: + xtick = peak.T[1] + if Page.plotStyle.get('flTicks',0) == 0: # short tick-marks + Page.tickDict[phase],_ = MPLaxes.plot( + xtick,pos,'|',mew=w,ms=l,picker=3., + label=phase,color=plcolor) + # N.B. above creates two Line2D objects, 2nd is ignored. + # Not sure what each does. + elif Page.plotStyle.get('flTicks',0) == 1: # full length tick-marks + # axvline changes plot limits, triggering onGroupXlimChanged + # so turn that off for now. + G2frame.stop_onGroupXlimChanged = True + if len(xtick) > 0: + # create an ~hidden tickmark to create a legend entry + Page.tickDict[phase] = MPLaxes.plot(xtick[0],0,'|',mew=0.5,ms=l, + label=phase,color=plcolor)[0] + for xt in xtick: # a separate line for each reflection position + MPLaxes.axvline(xt,color=plcolor, + picker=3., + label='_FLT_'+phase,lw=0.5) + del G2frame.stop_onGroupXlimChanged + +def showHistogram(G2frame,hist,Page,Plot): + 'Make a plot of a single histogram in a modal window' + # get histogram data + try: + pId = G2gd.GetGPXtreeItemId(G2frame, G2frame.root, hist) + if not pId: + print(f'Histogram {hist} not found. How did this happen?') + return + Pattern = G2frame.GPXtree.GetItemPyData(pId) + #Pattern.append(hist) + except: + print(f'Error accessing Histogram {hist}. How did this happen?') + return + #backDict = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,pId, 'Background'))[1] + try: + Parms,Parms2 = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,pId, 'Instrument Parameters')) + except TypeError: + Parms = None + limits = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,pId,'Limits')) + RefTbl = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,pId,'Reflection Lists')) + + dlg = wx.Dialog(G2frame,size=(650,550), + style=wx.DEFAULT_DIALOG_STYLE | wx.RESIZE_BORDER) + mainSizer = wx.BoxSizer(wx.VERTICAL) + mainSizer.Add( + wx.StaticText(dlg,wx.ID_ANY,'Quick view: '+hist, + style=wx.ALIGN_CENTER), + 0,wx.ALIGN_CENTER) + mainSizer.Add((-1,10)) + figure = mplfig.Figure(figsize=(6,8)) + canvas = G2plt.Canvas(dlg,-1,figure) + toolbar = G2plt.Toolbar(canvas) + toolbar.Realize() + #self.plotStyle = {'qPlot':False,'dPlot':False,'sqrtPlot':False,'sqPlot':False, + # 'logPlot':False,'exclude':False,'partials':True,'chanPlot':False} + + mainSizer.Add(canvas,1,wx.EXPAND) + mainSizer.Add(toolbar,0,) + + pwdrCol = {} + for i in 'Obs_color','Calc_color','Diff_color','Bkg_color': + pwdrCol[i] = '#' + GSASIIpath.GetConfigValue(i,getDefault=True) + #gs = mpl.gridspec.GridSpec(2, 1, height_ratios=[4, 1]) + #ax0 = figure.add_subplot(gs[0]) + #ax1 = figure.add_subplot(gs[1]) + ax0,ax1 = figure.subplots(2,1,sharex=True,gridspec_kw={'height_ratios':[4, 1],}) + + #figure.subplots_adjust(left=int(plotOpt['labelSize'])/100.,bottom=int(plotOpt['labelSize'])/150., + # right=.98,top=1.-int(plotOpt['labelSize'])/200.,hspace=0.0) + figure.subplots_adjust(left=11/100.,bottom=16/150., + right=.99,top=1.-3/200.,hspace=0,wspace=0) + ax0.tick_params('x',direction='in',labelbottom=False) + ax0.tick_params(labelsize=plotOpt['labelSize']) + ax1.tick_params(labelsize=plotOpt['labelSize']) + ax1.set_xlabel(Plot.get_xlabel(),fontsize=plotOpt['labelSize']) + ax0.set_ylabel(Plot.get_ylabel(),fontsize=plotOpt['labelSize']) + ax1.set_ylabel(r'$\Delta/\sigma$',fontsize=plotOpt['labelSize']) + + if Page.plotStyle['sqrtPlot']: + ax0.set_ylabel(r'$\sqrt{Intensity}$',fontsize=16) + else: + ax0.set_ylabel(r'$Intensity$',fontsize=16) + if Page.plotStyle['qPlot']: + xLabel = r'$Q, \AA^{-1}$' + elif Page.plotStyle['dPlot']: + xLabel = r'$d, \AA$' + elif 'T' in Parms['Type'][0]: + xLabel = r'$TOF, \mathsf{\mu}$s' + elif 'E' in Parms['Type'][0]: + xLabel = 'E, keV' + else: + xLabel = r'$\mathsf{2\theta}$' + ax1.set_xlabel(xLabel,fontsize=16) + lims = limits[1:] + # limit lines + ax0.axvline(lims[0][0],color='g',dashes=(5,5),picker=3.) + ax0.axvline(lims[0][1],color='r',dashes=(5,5),picker=3.) + # excluded region lines + for i,item in enumerate(lims[1:]): + Plot.axvline(item[0],color='m',dashes=(5,5),picker=3.) + Plot.axvline(item[1],color='m',dashes=(5,5),picker=3.) + pP = '+' + lW = 1.5 + if not G2frame.plusPlot: + pP = '' + lW = 1.5 + elif G2frame.plusPlot == 1: + pP = '+' + lW = 0 + elif G2frame.plusPlot == 2: # same as 0 for multiplot, TODO: rethink + pP = '' + lW = 1.5 + + #ExMask = np.full(len(xye[0]),False) + # recompute mask from excluded regions, in case they have changed + xye = np.array(ma.getdata(Pattern[1])) # strips mask = X,Yo,W,Yc,Yb,Yd + xye0 = xye[0] # no mask in case there are no limits + for excl in limits[2:]: + xye0 = ma.masked_inside(xye[0],excl[0],excl[1],copy=False) #excluded region mask + xye0 = ma.masked_outside(xye0,limits[1][0],limits[1][1],copy=False) #now mask for limits + if Page.plotStyle['qPlot']: + X = 2.*np.pi/G2lat.Pos2dsp(Parms,xye0) + lims = 2.*np.pi/G2lat.Pos2dsp(Parms,lims) + if 'T' in Parms['Type'][0]: xlim = (lims[0][1],lims[0][0]) + elif Page.plotStyle['dPlot']: + X = G2lat.Pos2dsp(Parms,xye0) + lims = G2lat.Pos2dsp(Parms,lims) + xlim = (lims[0][0],lims[0][1]) + else: + X = copy.deepcopy(xye0) + xlim = (lims[0][0],lims[0][1]) + #breakpoint() + Y = copy.copy(xye[1]) #yo + Z = copy.copy(xye[3]) #Yc + DZ = (xye[1]-xye[3])*np.sqrt(xye[2]) + + if G2frame.SubBack: + Y -= xye[4] #background subtract + Z -= xye[4] #background " + W = np.zeros_like(Y) #yb + elif Page.plotStyle['sqrtPlot']: + olderr = np.seterr(invalid='ignore') #get around sqrt(-ve) error + Y = np.where(Y>=0.,np.sqrt(Y),-np.sqrt(-Y)) + Z = np.where(Z>=0.,np.sqrt(Z),-np.sqrt(-Z)) + W = np.where(xye[4]>=0.,np.sqrt(xye[4]),-np.sqrt(-xye[4])) #yb + np.seterr(invalid=olderr['invalid']) + else: + W = xye[4] #yb + ax0.plot(X.data,Y,color=pwdrCol['Obs_color'],marker=pP,linewidth=lW, + clip_on=Clip_on,label='obs-bkg') #Io-Ib + if np.any(Z): #only if there is a calc pattern + ax0.plot(X,Z,pwdrCol['Calc_color'], + label='calc-bkg',linewidth=1.5) #Ic-Ib + ax0.plot(X,W,pwdrCol['Bkg_color'],picker=0.,label='calc',linewidth=1.5) + ax0.axhline(0.,color='k',label='_zero') + ax1.plot(X,DZ,pwdrCol['Diff_color'],label='diff') + drawTicks(G2frame,RefTbl,Page,ax0,group=True) + # zoom in to data limits with slight margin, but allow home button to zoom to full data range + toolbar.push_current() + r = xlim[1]-xlim[0] + ax0.set_xlim(xlim[0]-r/50,xlim[1]+r/50) + toolbar.push_current() + btnsizer = wx.StdDialogButtonSizer() + OKbtn = wx.Button(dlg, wx.ID_CLOSE) + OKbtn.Bind(wx.EVT_BUTTON,lambda event:dlg.EndModal(wx.ID_OK)) + OKbtn.SetDefault() + btnsizer.AddButton(OKbtn) + btnsizer.Realize() + mainSizer.Add(btnsizer,0,wx.TOP|wx.BOTTOM|wx.ALIGN_CENTER,1) + mainSizer.Layout() + dlg.SetSizer(mainSizer) + #mainSizer.Fit(dlg) + dlg.CenterOnParent() + dlg.ShowModal() + dlg.Destroy() diff --git a/GSASII/GSASIIrestrGUI.py b/GSASII/GSASIIrestrGUI.py index f6024609..f46d486f 100644 --- a/GSASII/GSASIIrestrGUI.py +++ b/GSASII/GSASIIrestrGUI.py @@ -214,7 +214,7 @@ def OnAddRestraint(event): parmDict = SetupParmDict(G2frame) dlg = G2exG.ExpressionDialog(G2frame,parmDict, header="Create a restraint expression", - fit=False,wildCard=G2frame.testSeqRefineMode()) + fit=False,wildCard=bool(G2frame.testSeqRefineMode(True))) restobj = dlg.Show(True) if restobj: restrData['General']['General'].append([restobj,0.0,1.0]) @@ -2192,7 +2192,7 @@ def OnEditGenRestraint(event): dlg = G2exG.ExpressionDialog(G2frame,parmDict, exprObj=generalRestData['General'][n][0], header="Edit a restraint expression", - fit=False,wildCard=G2frame.testSeqRefineMode()) + fit=False,wildCard=bool(G2frame.testSeqRefineMode(True))) restobj = dlg.Show(True) if restobj: generalRestData['General'][n][0] = restobj diff --git a/GSASII/GSASIIseqGUI.py b/GSASII/GSASIIseqGUI.py index 521cd5f2..44155911 100644 --- a/GSASII/GSASIIseqGUI.py +++ b/GSASII/GSASIIseqGUI.py @@ -62,7 +62,13 @@ def GetSampleParms(): '''Make a dictionary of the sample parameters that are not the same over the refinement series. Controls here is local ''' - if 'IMG' in histNames[0]: + Controls = G2frame.GPXtree.GetItemPyData( + G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + if histNames[0] in groupDict: + sampleParmDict = {'Temperature':[],'Pressure':[],'Time':[], + 'FreePrm1':[],'FreePrm2':[],'FreePrm3':[]} + elif 'IMG' in histNames[0]: sampleParmDict = {'Sample load':[],} elif 'PDF' in histNames[0]: sampleParmDict = {'Temperature':[]} @@ -70,25 +76,35 @@ def GetSampleParms(): sampleParmDict = {'Temperature':[],'Pressure':[],'Time':[], 'FreePrm1':[],'FreePrm2':[],'FreePrm3':[],'Omega':[], 'Chi':[],'Phi':[],'Azimuth':[],} - Controls = G2frame.GPXtree.GetItemPyData( - G2gd.GetGPXtreeItemId(G2frame,G2frame.root, 'Controls')) sampleParm = {} - for name in histNames: - if 'IMG' in name or 'PDF' in name: - if name not in data: - continue - for item in sampleParmDict: - sampleParmDict[item].append(data[name]['parmDict'].get(item,0)) - else: - if 'PDF' in name: - name = 'PWDR' + name[4:] - Id = G2gd.GetGPXtreeItemId(G2frame,G2frame.root,name) - if Id: - sampleData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,'Sample Parameters')) - else: # item missing from tree! stick in NaN's! - sampleData = {} - for item in sampleParmDict: - sampleParmDict[item].append(sampleData.get(item,np.nan)) + for ng in histNames: + vals = {item:[] for item in sampleParmDict} + for name in groupDict.get(ng,[ng]): + if 'IMG' in name or 'PDF' in name: + if name not in data: + continue + for item in sampleParmDict: + vals[item].append(data[name]['parmDict'].get(item,0)) + else: + if 'PDF' in name: + name = 'PWDR' + name[4:] + Id = G2gd.GetGPXtreeItemId(G2frame,G2frame.root,name) + if Id: + sampleData = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,Id,'Sample Parameters')) + else: # item missing from tree! stick in NaN's! + sampleData = {} + for item in sampleParmDict: + vals[item].append(sampleData.get(item,np.nan)) + # for a group, are all the values the same? + for item in vals: + try: + same = np.allclose(vals[item],vals[item][0]) + except: + same = False + val = np.nan + if same: val = vals[item][0] + sampleParmDict[item].append(val) + # now compare values across all the entries in the table for item in sampleParmDict: if sampleParmDict[item]: frstValue = sampleParmDict[item][0] @@ -137,7 +153,28 @@ def PlotSelectedColRow(calltyp='',event=None): SetLabelString(cols[0]) #only the 1st one selected! elif rows and calltyp == 'left': name = histNames[rows[0]] #only does 1st one selected - if name.startswith('PWDR'): + if name in groupDict: + if name not in groupClickCount: return # should not happen + groupClickCount[name] += 1 + print('click count',groupClickCount[name]) + if groupClickCount[name] == 1 or groupClickCount[name] > len(groupDict[name])+1: + print('plot group') + item = G2gd.GetGPXtreeItemId(G2frame, G2frame.root, 'Groups/Powder') + item = G2gd.GetGPXtreeItemId(G2frame, item, name) + if item: + if not hasattr(G2frame,'GroupInfo'): + G2frame.GroupInfo = {} + G2frame.GroupInfo['groupName'] = name # also set in GSASIIgroupGUI.UpdateGroup + G2pwpl.PlotPatterns(G2frame,newPlot=True,plotType='GROUP') + groupClickCount[name] = 1 + else: + h = groupDict[name][groupClickCount[name]-2] + print('plot hist',h) + pickId = G2frame.PickId + G2frame.PickId = G2frame.PatternId = G2gd.GetGPXtreeItemId(G2frame, G2frame.root, h) + G2pwpl.PlotPatterns(G2frame,newPlot=False,plotType='PWDR') + G2frame.PickId = pickId + elif name.startswith('PWDR'): pickId = G2frame.PickId G2frame.PickId = G2frame.PatternId = G2gd.GetGPXtreeItemId(G2frame, G2frame.root, name) G2pwpl.PlotPatterns(G2frame,newPlot=False,plotType='PWDR') @@ -633,14 +670,6 @@ def WriteSeq(): finally: dlg.Destroy() - def striphist(var,insChar=''): - 'strip a histogram number from a var name' - sv = var.split(':') - if len(sv) <= 1: return var - if sv[1]: - sv[1] = insChar - return ':'.join(sv) - def plotSpCharFix(lbl): 'Change selected unicode characters to their matplotlib equivalent' for u,p in [ @@ -710,7 +739,7 @@ def EditPseudoVar(event): data['SeqPseudoVars'][choices[selected]], header="Edit the PseudoVar expression", VarLabel="PseudoVar #"+str(selected+1), - fit=False) + fit=False,wildCard=(not bool(groupDict))) newobj = dlg.Show(True) if newobj: calcobj = G2obj.ExpressionCalcObj(newobj) @@ -722,7 +751,8 @@ def AddNewPseudoVar(event): 'Create a new pseudo var expression' dlg = G2exG.ExpressionDialog(G2frame.dataDisplay,PSvarDict, header='Enter an expression for a PseudoVar here', - VarLabel = "New PseudoVar",fit=False) + VarLabel = "New PseudoVar",fit=False, + wildCard=(not bool(groupDict))) obj = dlg.Show(True) dlg.Destroy() if obj: @@ -895,6 +925,76 @@ def EvalPSvarDeriv(calcobj,parmDict,sampleDict,var,ESD): return None return (results[0] - results[1]) / (2.*step) + # def EvalPSvarDerivOpt(calcobj,parmDict,sampleDict,var,ESD): + # '''Evaluate an expression derivative with respect to a + # GSAS-II variable name. This is an optimized version of EvalPSvarDeriv + # that only changes elements in the calcobj dict + # + # TODO: This needs to be debugged (problem in calcobj.UpdateCalc?) + # ''' + # if not ESD: + # return 0. + # step = ESD/10 + # Ddict = dict(zip(['D11','D22','D33','D12','D13','D23'], + # ['A'+str(i) for i in range(6)]) + # ) + # results = [] + # phaselist = [] + # VparmDict = sampleDict.copy() + # VparmDict.update(parmDict.copy()) + # VparmDict.update({item:0.0 for item in parmDict if 'dA' in item}) + # calcobj.SetupCalc(VparmDict) + # changeDict = {} + # for incr in step,-step: + # # as saved, the parmDict has updated 'A[xyz]' values, but 'dA[xyz]' + # # values are not zeroed: fix that! + # VparmDict[var] += incr + # changeDict[var] = VparmDict[var] + # G2mv.Dict2Map(VparmDict) # apply constraints + # # generate the atom positions and the direct & reciprocal cell values now, because they might + # # needed to evaluate the pseudovar + # for item in VparmDict: + # if item in sampleDict: + # continue + # if ':' not in item: continue + # key = item.split(':') + # if len(key) < 3: continue + # # apply any new shifts to atom positions + # if key[2].startswith('dA'): + # VparmDict[''.join(item.split('d'))] += VparmDict[item] + # VparmDict[item] = 0.0 + # changeDict[item] = VparmDict[item] + # # compute and update the corrected reciprocal cell terms using the Dij values + # if key[2] in Ddict: + # if key[0] not in phaselist: phaselist.append(key[0]) + # akey = key[0]+'::'+Ddict[key[2]] + # VparmDict[akey] += VparmDict[item] + # changeDict[akey] = VparmDict[akey] + # for i in phaselist: + # pId = int(i) + # # apply cell symmetry + # try: + # A,zeros = G2stIO.cellFill(str(pId)+'::',SGdata[pId],VparmDict,zeroDict[pId]) + # # convert to direct cell & add the unique terms to the dictionary + # for i,val in enumerate(G2lat.A2cell(A)): + # if i in uniqCellIndx[pId]: + # lbl = str(pId)+'::'+G2lat.cellUlbl[i] + # VparmDict[lbl] = val + # changeDict[lbl] = VparmDict[lbl] + # lbl = str(pId)+'::'+'Vol' + # VparmDict[lbl] = G2lat.calc_V(A) + # changeDict[lbl] = VparmDict[lbl] + # except KeyError: + # pass + # # dict should be fully updated, use it & calculate + # #calcobj.SetupCalc(VparmDict) + # calcobj.UpdateCalc(changeDict) # apply changes + # results.append(calcobj.EvalExpression()) + # if None in results: + # return None + # return (results[0] - results[1]) / (2.*step) + + def EnableParFitEqMenus(): 'Enables or disables the Parametric Fit menu items that require existing defs' if data['SeqParFitEqList']: @@ -1303,6 +1403,30 @@ def OnEditSelectPhaseVars(event): wx.CallAfter(UpdateSeqResults,G2frame,data) # redisplay variables return + def mapHistVar(var,hist): + '''Replace a histogram number in a var name with the first in the series + or a * if not a grouped fit + ''' + sv = var.split(':') + if len(sv) <= 1: return var + if groupDict and hist in histReplDict: + replVal = histReplDict[hist].get(var.split(':')[1],'?') + elif groupDict: + return var + else: + replVal = '*' + if sv[1]: + sv[1] = replVal + return ':'.join(sv) + + def striphist(var): + 'strip a histogram number from a var name' + sv = var.split(':') + if len(sv) <= 1: return var + if sv[1]: + sv[1] = '' + return ':'.join(sv) + #---- UpdateSeqResults: start processing sequential results here ########## # lookup table for unique cell parameters by symmetry if not data: @@ -1316,16 +1440,33 @@ def OnEditSelectPhaseVars(event): histNames = [name for name in data['histNames']] Controls = {} ifPWDR = False + groupDict = {} else: Controls = G2frame.GPXtree.GetItemPyData(G2gd.GetGPXtreeItemId(G2frame,G2frame.root,'Controls')) # create a place to store Pseudo Vars & Parametric Fit functions, if not present if 'SeqPseudoVars' not in data: data['SeqPseudoVars'] = {} if 'SeqParFitEqList' not in data: data['SeqParFitEqList'] = [] histNames = [name for name in data['histNames'] if name in data] + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + groupClickCount = {name:0 for name in groupDict} # counts number of times an entry has been clicked + #groupSeqSel = Controls['Groups'].get('groupSeqSel',list(groupDict.keys())) + # create the dict needed by mapHistVar + firsthId = [] + histReplDict = {} + for name in groupDict: + histReplDict[name] = {} + histList = groupDict.get(name,[name]) + hIdlist = [str(Histograms[h]['hId']) for h in histList] + if not firsthId: firsthId = hIdlist + if len(firsthId) == len(hIdlist): + histReplDict[name] = dict(zip(hIdlist,firsthId)) + + if len(histNames) == 0: + print ('No entries in sequential refinement results') + return if G2frame.dataDisplay: G2frame.dataDisplay.Destroy() G2frame.GetStatusBar().SetStatusText("Select column to export; LMB/RMB column to plot data/change label; LMB/RMB on row for PWDR/Covariance plot",1) - sampleParms = GetSampleParms() # get unit cell & symmetry for all phases & initial stuff for later use RecpCellTerms = {} @@ -1395,26 +1536,27 @@ def OnEditSelectPhaseVars(event): maxPWL = 5 # number of Pawley vars to show missing = 0 newCellDict = {} + sampleParms = GetSampleParms() PSvarDict = {} # contains 1st value for each parameter in parmDict # needed for creating & editing pseudovars PSvarDict.update(sampleParms) # scan through histograms to see what are available and to make a - # list of all varied parameters; also create a dict that has the + # list of all varied parameters; also create a dict that has the atoms for i,name in enumerate(histNames): if name not in data: if missing < 5: - print(" Warning: "+name+" not found") + print(f' Warning: entry "{name}" not found') elif missing == 5: - print (' Warning: more are missing') + print(f' Warning: entry "{name}" not found...') + #print (' Warning: more are missing') missing += 1 continue foundHistNames.append(name) for var,val,sig in zip(data[name]['varyList'],data[name]['variables'],data[name]['sig']): - svar = striphist(var,'*') # wild-carded + svar = mapHistVar(var,name) # replace/remap histogram number, if present if 'PWL' in svar: - if int(svar.split(':')[-1]) > maxPWL: - continue + if int(svar.split(':')[-1]) > maxPWL: continue # limit # of Pawley reflections if svar not in combinedVaryList: # add variables to list as they appear combinedVaryList.append(svar) @@ -1426,7 +1568,7 @@ def OnEditSelectPhaseVars(event): newCellDict.update(data[name].get('newCellDict',{})) # N.B. These Dij vars are missing a histogram # # make sure 1st reference to each parm is in PseudoVar dict tmp = copy.deepcopy(data[name].get('parmDict',{})) - tmp = {striphist(var,'*'):tmp[var] for var in tmp} # replace histogram #s with "*" + tmp = {mapHistVar(var,name):tmp[var] for var in tmp} # replace histogram # tmp.update(PSvarDict) PSvarDict = tmp @@ -1449,20 +1591,25 @@ def OnEditSelectPhaseVars(event): combinedVaryList.sort() histNames = foundHistNames # prevVaryList = [] + histNumList = [] # numbers to put on each row, if empty replace with numbers + rowLabels = [] + if groupDict and max([len(i) for i in groupDict])>12: # for long group names drop the letters that are the same for every label + cols = zip(*groupDict.keys()) + keep_mask = [len(set(col)) > 1 for col in cols] + rowLabels = ["".join(ch for ch, keep in zip(s, keep_mask) if keep) for s in groupDict.keys()] + else: + for i,name in enumerate(histNames): + if name in Histograms: + histNumList.append(list(Histograms.keys()).index(name)) posdict = {} # defines position for each entry in table; for inner # dict key is column number & value is parameter name - histNumList = [] for i,name in enumerate(histNames): - if name in Histograms: - histNumList.append(list(Histograms.keys()).index(name)) - # if prevVaryList != data[name]['varyList']: # this refinement has a different refinement list from previous - # prevVaryList = data[name]['varyList'] posdict[name] = {} for var in data[name]['varyList']: - svar = striphist(var,'*') + svar = mapHistVar(var,name) # wild-card or map to 1st in group if 'PWL' in svar: - if int(svar.split(':')[-1]) > maxPWL: - continue + if int(svar.split(':')[-1]) > maxPWL: continue + if svar not in combinedVaryList: continue ### TODO: need to figure out what this loop is doing posdict[name][combinedVaryList.index(svar)] = svar ####-- build up the data table by columns ----------------------------------------------- nRows = len(histNames) @@ -1601,10 +1748,9 @@ def OnEditSelectPhaseVars(event): for ih,name in enumerate(histNames): varsellist = [posdict[name].get(i) for i in range(len(combinedVaryList))] # translate variable names to how they will be used in the headings - vs = [striphist(v,'*') for v in data[name]['varyList']] + vs = [mapHistVar(v,name) for v in data[name]['varyList']] # determine the index for each column (or None) in the data[]['variables'] and ['sig'] lists sellist = [vs.index(v) if v is not None else None for v in varsellist] - #sellist = [i if striphist(v,'*') in varsellist else None for i,v in enumerate(data[name]['varyList'])] if not varsellist: raise Exception() vals.append([data[name]['variables'][s] if s is not None else None for s in sellist]) #replace mode displacement shift with value; esd applies to both @@ -1643,7 +1789,7 @@ def OnEditSelectPhaseVars(event): for var in data[name].get('depParmDict',{}): if '::dA' in var: continue val,sig = data[name]['depParmDict'][var] - svar = striphist(var,'*') + svar = mapHistVar(var,name) if svar not in depValDict: depValDict[svar] = {} depValDict[svar][name] = (val,sig) @@ -1681,6 +1827,7 @@ def OnEditSelectPhaseVars(event): if msg: print('Unable to interpret multiplier(s) for',name,':',msg) continue + G2mv.ProcessOffsets({}) G2mv.GenerateConstraints(varyList,constrDict,fixedList,parmDict, seqHistNum=ihst,raiseException=False) if 'Dist' in expr: @@ -1701,6 +1848,9 @@ def OnEditSelectPhaseVars(event): VCoV = G2mth.getVCov(varyNames,varyList,data[name]['covMatrix']) esdList.append(np.sqrt(np.inner(derivs,np.inner(VCoV,derivs.T)) )) else: + # derivs = np.array( # TODO: this needs to be debugged and used to replace EvalPSvarDeriv + # [EvalPSvarDerivOpt(calcobj,parmDict.copy(),sampleDict[name],var,ESD) + # for var,ESD in zip(varyList,sigs)]) derivs = np.array( # TODO: this needs to be reworked [EvalPSvarDeriv(calcobj,parmDict.copy(),sampleDict[name],var,ESD) for var,ESD in zip(varyList,sigs)]) @@ -1714,13 +1864,15 @@ def OnEditSelectPhaseVars(event): np.inner(derivs,np.inner(data[name]['covMatrix'],derivs.T)) )) except: esdList.append(None) - psDict = parmDict.copy() + #psDict = parmDict.copy() + # for grouped fits, replace the var names with the 1st histogram #s + psDict = {mapHistVar(var,name):val for var,val in parmDict.items()} psDict.update(sampleDict[name]) try: UpdateParmDict(psDict) except: print('UpdateParmDict error on histogram',name) - calcobj.UpdateDict(psDict) + calcobj.SetupCalc(psDict) valList.append(calcobj.EvalExpression()) # if calcobj.su is not None: esdList[-1] = calcobj.su if not esdList: @@ -1793,7 +1945,9 @@ def OnEditSelectPhaseVars(event): mainSizer = wx.BoxSizer(wx.VERTICAL) mainSizer.Add(G2frame.dataDisplay,1,wx.EXPAND,1) G2frame.dataWindow.SetSizer(mainSizer) - if histNames[0].startswith('PWDR'): + if len(rowLabels) == len(histNames): + pass + elif histNames[0].startswith('PWDR'): #rowLabels = [str(i)+': '+l[5:30] for i,l in enumerate(histNames)] rowLabels = [l[5:] for i,l in enumerate(histNames)] else: @@ -1818,7 +1972,7 @@ def OnEditSelectPhaseVars(event): # highlight unconverged shifts if histNames[0][:4] not in ['SASD','IMG ','REFD',] and deltaChiCol is not None: for row,name in enumerate(histNames): - if name not in Controls.get('Seq Data',{}): + if name not in Controls.get('Seq Data',{}) and not groupDict: G2frame.dataDisplay.SetCellTextColour(row,0,wx.Colour(255,0,0)) deltaChi = G2frame.SeqTable.GetValue(row,deltaChiCol) try: @@ -2406,4 +2560,4 @@ def ExportSequentialImages(G2frame,histNames,outdir,dpi='figure'): f = os.path.join(outdir,f"{histNames[i].replace('.', '_').replace(' ', '_')}.png") files.append(f) Plot.figure.savefig(f,dpi=dpi) - return files + return files \ No newline at end of file diff --git a/GSASII/GSASIIstrIO.py b/GSASII/GSASIIstrIO.py index e48f8239..a95d2670 100644 --- a/GSASII/GSASIIstrIO.py +++ b/GSASII/GSASIIstrIO.py @@ -128,11 +128,61 @@ def GetControls(GPXfile): Controls.update(datum[1]) return Controls -def ReadConstraints(GPXfile, seqHist=None): +def groupEquivTbl(key,groupDict,Histograms,warn=False): + '''For grouped sequential fits, create a table of histogram hId numbers + that match sequentially to those in the current group. The dict created + here is used to generate substitutions in constraints. Note that only + groups with the same number of histograms as the current are considered. + + :param str key: name associated with current group + :param dict groupDict: dict with histograms in each group + :param dict Histograms: dict with all histograms + :param bool warn: if True, messages are sent to console when a referenced + histogram is not found. + :returns: a dict (grEquivTbl) and a list (grHIDlist), where + for grEquivTbl the key is a hId # that is the same sequence number + within a different group than the present and the associated value is + the hId number in the current group; + and grHIDlist is a list of hId values for the histograms referenced + by key (groupDict[key]) + ''' + grEquivTbl = {} + grHIDlist = [] + for i,h in enumerate(groupDict[key]): + if h not in Histograms: + if warn: print(f'Grouped Sequential histogram {h} not found') + continue + hId = Histograms[h]['hId'] + grHIDlist.append(hId) + #print(f'equiv to {hId} {h}') + for key1 in groupDict: + if key == key1: continue + if len(groupDict[key]) != len(groupDict[key1]): continue + h1 = groupDict[key1][i] + if h1 not in Histograms: + if warn: print(f'Grouped Sequential histogram {h1} not found') + continue + hId1 = Histograms[h1]['hId'] + #print(f'... {hId1} {h1}') + grEquivTbl[hId1] = hId + return grEquivTbl,grHIDlist + +def ReadConstraints(GPXfile, seqHist=None, grEquivTbl=None, grHIDlist=None): '''Read the constraints from the GPX file and interpret them - called in :func:`ReadCheckConstraints`, :func:`GSASIIstrMain.Refine` + Called in :func:`ReadCheckConstraints`, :func:`GSASIIstrMain.Refine` and :func:`GSASIIstrMain.SeqRefine`. + + :param str GPXfile: GSAS-II project file name + :param int seqHist: defines a specific histogram that is used in a + (non-grouped) sequential refinement. None for a non-sequential or a + grouped sequential fit. + :param dict grEquivTbl: for grouped sequential refinements: converts + histogram numbers (hId) that are in other histogram + groups to one in the current group + :param list grHIDlist: for grouped sequential refinements: is a list of + hId values for the current group of histograms + :returns: constrDict,fixedList with evaluated constraint information ''' IndexGPX(GPXfile) fl = open(GPXfile,'rb') @@ -152,25 +202,35 @@ def ReadConstraints(GPXfile, seqHist=None): d = {str(k):v for k,v in zip(ConstraintsItem[1].get('_OffsetKeys',[]), ConstraintsItem[1].get('_OffsetVals',[]))} G2mv.ProcessOffsets(d) - constrDict,fixedList,ignored = G2mv.ProcessConstraints(constList,seqmode,seqHist) + constrDict,fixedList,ignored = G2mv.ProcessConstraints(constList,seqmode,seqHist,grEquivTbl,grHIDlist) #if ignored: # G2fil.G2Print ('Warning: {} Constraints were rejected. Was a constrained phase, histogram or atom deleted?'.format(ignored)) return constrDict,fixedList -def ReadCheckConstraints(GPXfile, seqHist=None,Histograms=None,Phases=None): - '''Load constraints and related info and return any error or warning messages - This is done from the GPX file rather than the tree. +def ReadCheckConstraints(GPXfile, seqHist=None,Histograms=None,Phases=None, + grHistList=None,grEquivTbl=None): + '''Load constraints and related info and return any error or + warning messages prior to a refinement. + This is done from the GPX file rather than the tree. It is similar + to GSASIIconstrGUI.CheckConstraint, which instead reads from the + data tree. :param str GPXfile: specifies the path to a .gpx file. - :param str seqHist: specifies a histogram to be loaded for - a sequential refinement. If None (default) all are loaded. - :param dict Histograms: output from :func:`GetUsedHistogramsAndPhases`, + :param str/list seqHist: can be the name of a histogram or can be a list + of histograms that will be fit together in a grouped sequential fit. + :param dict Histograms: output from :func:`GetUsedHistogramsAndPhases`; can optionally be supplied to save time for sequential refinements - :param dict Phases: output from :func:`GetUsedHistogramsAndPhases`, can + where reading this can take a fair amount of time + :param dict Phases: output from :func:`GetUsedHistogramsAndPhases`; can optionally be supplied to save time for sequential refinements + :param list grHistList: for grouped sequential refinements: a list of + the histogram names in the current refinement group. + :param dict grEquivTbl: for grouped sequential refinements: converts + histogram numbers (hId) that are in other histogram + groups to one in the current group ''' G2mv.InitVars() # init constraints - # get variables + # get input if not already read if Histograms is None or Phases is None: Histograms,Phases = GetUsedHistogramsAndPhases(GPXfile) if not Phases: @@ -180,17 +240,29 @@ def ReadCheckConstraints(GPXfile, seqHist=None,Histograms=None,Phases=None): if seqHist: Histograms = {seqHist:Histograms[seqHist]} # sequential fit: only need one histogram hId = Histograms[seqHist]['hId'] + constrDict,fixedList = ReadConstraints(GPXfile,hId) # load user constraints from file (uses ProcessConstraints) + elif grHistList: + grHIDlist = [Histograms[i]['hId'] for i in grHistList] + Histograms = {i:Histograms[i] for i in grHistList} # sequential fit: only need one histogram + constrDict,fixedList = ReadConstraints(GPXfile,grEquivTbl=grEquivTbl,grHIDlist=grHIDlist) # load user constraints from file (uses ProcessConstraints) else: hId = None - constrDict,fixedList = ReadConstraints(GPXfile, hId) # load user constraints from file (uses ProcessConstraints) + constrDict,fixedList = ReadConstraints(GPXfile) # load user constraints from file (uses ProcessConstraints) parmDict = {} # generate symmetry constraints to check for conflicts rigidbodyDict = GetRigidBodies(GPXfile) rbIds = rigidbodyDict.get('RBIds',{'Vector':[],'Residue':[],'Spin':[]}) rbVary,rbDict = GetRigidBodyModels(rigidbodyDict,Print=False) parmDict.update(rbDict) - (Natoms, atomIndx, phaseVary,phaseDict, pawleyLookup,FFtables,EFtables,ORBtables,BLtables,MFtables,maxSSwave) = \ - GetPhaseData(Phases,RestraintDict=None,seqHistName=seqHist,rbIds=rbIds,Print=False) # generates atom symmetry constraints + if seqHist: + (Natoms, atomIndx, phaseVary,phaseDict, pawleyLookup,FFtables,EFtables,ORBtables,BLtables,MFtables,maxSSwave) = \ + GetPhaseData(Phases,RestraintDict=None,seqHistName=seqHist,rbIds=rbIds,Print=False) # generates atom symmetry constraints + elif grHistList: + (Natoms, atomIndx, phaseVary,phaseDict, pawleyLookup,FFtables,EFtables,ORBtables,BLtables,MFtables,maxSSwave) = \ + GetPhaseData(Phases,RestraintDict=None,rbIds=rbIds,Print=False,grHistList=grHistList) # generates atom symmetry constraints + else: + (Natoms, atomIndx, phaseVary,phaseDict, pawleyLookup,FFtables,EFtables,ORBtables,BLtables,MFtables,maxSSwave) = \ + GetPhaseData(Phases,RestraintDict=None,rbIds=rbIds,Print=False) # generates atom symmetry constraints parmDict.update(phaseDict) hapVary,hapDict,controlDict = GetHistogramPhaseData(Phases,Histograms,Print=False,resetRefList=False) parmDict.update(hapDict) @@ -199,8 +271,13 @@ def ReadCheckConstraints(GPXfile, seqHist=None,Histograms=None,Phases=None): varyList = rbVary+phaseVary+hapVary+histVary msg = G2mv.EvaluateMultipliers(constrDict,phaseDict,hapDict,histDict) if msg: - return 'Unable to interpret multiplier(s): '+msg,'' - errmsg,warnmsg,groups,parmlist = G2mv.GenerateConstraints(varyList,constrDict,fixedList,parmDict,seqHistNum=hId) + return f'Unable to interpret multiplier(s): {msg}' + if grHistList: + errmsg,warnmsg,groups,parmlist = G2mv.GenerateConstraints(varyList,constrDict,fixedList,parmDict,seqHistNum=None, + grEquivTbl=grEquivTbl,grHIDlist=grHIDlist) + else: + errmsg,warnmsg,groups,parmlist = G2mv.GenerateConstraints(varyList,constrDict,fixedList,parmDict,seqHistNum=hId) + G2mv.Map2Dict(parmDict,varyList) # changes varyList return errmsg, warnmsg @@ -575,15 +652,15 @@ def GetUsedHistogramsAndPhases(GPXfile): # renamed or deleted G2fil.G2Print('Warning: For phase "'+phase+ '" unresolved reference to histogram "'+hist+'"') - # load the fix background info into the histograms + # load the fixed background values into the histograms for hist in Histograms: if 'Background' not in Histograms[hist]: continue - fixedBkg = Histograms[hist]['Background'][1].get('background PWDR') - if fixedBkg: - if not fixedBkg[0]: continue + h = Histograms[hist]['Background'][1] + h['fixback'] = None # fixed background is computed here when needed; best if never saved + fixedBkg = h['background PWDR'] = h.get('background PWDR',['',1.,False]) + if fixedBkg and fixedBkg[0]: # patch: add refinement flag, if needed if len(fixedBkg) == 2: fixedBkg += [False] - h = Histograms[hist]['Background'][1] try: Limits = Histograms[hist]['Limits'][1] x = Histograms[hist]['Data'][0] @@ -795,40 +872,38 @@ def SaveUpdatedHistogramsAndPhases(GPXfile,Histograms,Phases,RigidBodies,CovData pickle.dump([['Rigid bodies',RigidBodies]],fp,1) pickle.dump([['parmFrozen',parmFrozen]],fp,1) fp.close() - # create an entry that looks like a PWDR tree item - for key in Histograms: - if key.startswith('PWDR '): - break - else: - raise Exception('No PWDR entry in Histogram dict!') - histname = key - hist = copy.deepcopy(Histograms[key]) - xfer_dict = {'Index Peak List': [[], []], - 'Comments': [], - 'Unit Cells List': [], - 'Peak List': {'peaks': [], 'sigDict': {}}, - } - histData = hist['Data'] - del hist['Data'] - for key in ('Limits','Background','Instrument Parameters', - 'Sample Parameters','Reflection Lists'): - xfer_dict[key] = hist[key] - if key == 'Background': # remove fixed background from file - xfer_dict['Background'][1] = {k:hist['Background'][1][k] - for k in hist['Background'][1] - if not k.startswith('_fixed')} - del hist[key] - # xform into a gpx-type entry - data = [] - data.append([histname,[hist,histData,histname]]) - for key in ['Comments','Limits','Background','Instrument Parameters', - 'Sample Parameters','Peak List','Index Peak List', - 'Unit Cells List','Reflection Lists']: - data.append([key,xfer_dict[key]]) - # append histogram to histogram info + histList = [h for h in Histograms if h.startswith('PWDR ')] + if len(histList) == 0: + raise Exception('SaveUpdatedHistogramsAndPhases: No PWDR entry in Histogram dict!') + # create an entry that looks like a PWDR tree item for each histogram GPXhist = os.path.splitext(GPXfile)[0]+'.seqHist' fp = open(GPXhist,'ab') - pickle.dump(data,fp,1) + for histname in histList: + hist = copy.deepcopy(Histograms[histname]) + xfer_dict = {'Index Peak List': [[], []], + 'Comments': [], + 'Unit Cells List': [], + 'Peak List': {'peaks': [], 'sigDict': {}}, + } + histData = hist['Data'] + del hist['Data'] + for key in ('Limits','Background','Instrument Parameters', + 'Sample Parameters','Reflection Lists'): + xfer_dict[key] = hist[key] + if key == 'Background': # remove fixed background from file + xfer_dict['Background'][1] = {k:hist['Background'][1][k] + for k in hist['Background'][1] + if not k.startswith('_fixed')} + del hist[key] + # xform into a gpx-type entry + data = [] + data.append([histname,[hist,histData,histname]]) + for key in ['Comments','Limits','Background','Instrument Parameters', + 'Sample Parameters','Peak List','Index Peak List', + 'Unit Cells List','Reflection Lists']: + data.append([key,xfer_dict[key]]) + # append histogram to histogram info + pickle.dump(data,fp,1) fp.close() return @@ -1059,6 +1134,7 @@ def PrintVecRBModel(RBModel): pFile.write(i) pFile.write('Orientation defined by: atom %s -> atom %s & atom %s -> atom %s\n'% (RBModel['rbRef'][0],RBModel['rbRef'][1],RBModel['rbRef'][0],RBModel['rbRef'][2])) + if Print and pFile is None: raise Exception("specify pFile or Print=False") rbVary = [] rbDict = {} @@ -1127,7 +1203,7 @@ def PrintRBVectandSig(VectRB,VectSig): ##### Phase data ################################################################################ def GetPhaseData(PhaseData,RestraintDict={},rbIds={},Print=True,pFile=None, - seqHistName=None,symHold=None): + seqHistName=None,symHold=None,grHistList=None): '''Setup the phase information for a structural refinement, used for regular and sequential refinements, optionally printing information to the .lst file (if Print is True). Used as part of refinements but also @@ -1138,7 +1214,9 @@ def GetPhaseData(PhaseData,RestraintDict={},rbIds={},Print=True,pFile=None, dict(s) from the tree. :param dict PhaseData: the contents of the Phase tree item (may be read from - .gpx file) with information on all phases + .gpx file) with information on all phases, or if not all phases are + needed (for a sequential fit), this should contain only the phase(s) + that will be needed for the current histogram(s) :param dict RestraintDict: an optional dict with restraint information :param dict rbIds: an optional dict with rigid body information :param bool Print: a flag that determines if information will be formatted and @@ -1152,6 +1230,8 @@ def GetPhaseData(PhaseData,RestraintDict={},rbIds={},Print=True,pFile=None, :param list symHold: if not None (None is the default) the names of parameters held due to symmetry are placed in this list even if not varied. (Used in G2constrGUI and for parameter impact estimates in AllPrmDerivs). + :param list grHistList: for grouped sequential refinements: a list of + the histogram names in the current refinement group. :returns: lots of stuff: Natoms,atomIndx,phaseVary,phaseDict,pawleyLookup, FFtables,EFtables,ORBtables,BLtables,MFtables,maxSSwave (see code for details). ''' @@ -1664,7 +1744,13 @@ def MakeRBSphHarm(rbKey,phaseVary,phaseDict): SamSym = dict(zip(shModels,['0','-1','2/m','mmm'])) atomIndx = {} for name in PhaseData: - if seqHistName is not None and seqHistName != 'All': # sequential: load only used phases + if grHistList is not None: # only select phases used in one of the current group's histograms + for h in grHistList: + if h in PhaseData[name]['Histograms'] and PhaseData[name]['Histograms'][h]['Use']: + break + else: + continue + elif seqHistName is not None and seqHistName != 'All': # sequential: load only phases used in current histogram if seqHistName not in PhaseData[name]['Histograms']: continue if not PhaseData[name]['Histograms'][seqHistName]['Use']: continue General = PhaseData[name]['General'] @@ -1928,8 +2014,7 @@ def MakeRBSphHarm(rbKey,phaseVary,phaseDict): phaseVary.append(pfx+item) if Print: - pFile.write('\n Phase name: %s\n'%General['Name']) - pFile.write(135*'='+'\n') + pFile.write(f"\n Phase name: {General['Name']}\n{135*'-'}\n") PrintFFtable(FFtable) PrintEFtable(EFtable) PrintORBtable(ORBtable) @@ -1973,9 +2058,7 @@ def MakeRBSphHarm(rbKey,phaseVary,phaseDict): elif PawleyRef: if Print: - pFile.write('\n Phase name: %s\n'%General['Name']) - pFile.write(135*'='+'\n') - pFile.write('\n') + pFile.write(f"\n Phase name: {General['Name']}\n{135*'-'}\n\n") if len(SSGtext): #if superstructure for line in SSGtext: pFile.write(line+'\n') if len(SSGtable): @@ -2512,8 +2595,7 @@ def PrintRestraints(cell,AtPtrs,phaseRest): #### SetPhaseData starts here if pFile: pFile.write('\n Phases:\n') for phase in Phases: - if pFile: pFile.write(' Result for phase: %s\n'%phase) - if pFile: pFile.write(135*'='+'\n') + if pFile: pFile.write(f' Result for phase: {phase}\n{135*"-"}\n') Phase = Phases[phase] General = Phase['General'] SGData = General['SGData'] @@ -2695,9 +2777,6 @@ def PrintRestraints(cell,AtPtrs,phaseRest): if pfx+name in sigDict: wavesSig[name] = sigDict[pfx+name] - - - Deformations = Phase.get('Deformations',{}) for iAt in Deformations: if iAt < 0: @@ -3106,8 +3185,7 @@ def PrintBabinet(hapData): hapVary.append(pfx+bab) if Print: - pFile.write('\n Phase: %s in histogram: %s\n'%(phase,histogram)) - pFile.write(135*'='+'\n') + pFile.write(f'\n Phase: "{phase}" in histogram: "{histogram}"\n{135*"-"}\n') if hapDict.get(pfx+'LeBail'): pFile.write(' Perform LeBail extraction\n') elif 'E' not in inst['Type'][0]: @@ -3272,8 +3350,7 @@ def PrintBabinet(hapData): if len(Twins) > 1: #force sum to unity hapDict[pfx+'TwinFr:0'] = 1.-sumTwFr if Print: - pFile.write('\n Phase: %s in histogram: %s\n'%(phase,histogram)) - pFile.write(135*'='+'\n') + pFile.write(f'\n Phase: "{phase}" in histogram: "{histogram}"\n{135*"-"}\n') pFile.write(' Scale factor : %10.4g Refine? %s\n'%(hapData['Scale'][0],hapData['Scale'][1])) if extType != 'None': pFile.write(' Extinction Type: %15s approx: %10s\n'%(extType,extApprox)) @@ -3624,8 +3701,7 @@ def PrintTwinsAndSig(pfx,twinData,TwinSig): hfx = ':%s:'%(hId) if pfx+'Nref' not in Histogram['Residuals']: #skip not used phase in histogram continue - pFile.write('\n Phase: %s in histogram: %s\n'%(phase,histogram)) - pFile.write(135*'='+'\n') + pFile.write(f'\n Phase: "{phase}" in histogram: "{histogram}"\n{135*"-"}\n') Inst = Histogram['Instrument Parameters'][0] if 'PWDR' in histogram: pFile.write(' Final refinement RF, RF^2 = %.2f%%, %.2f%% on %d reflections\n'% @@ -3940,8 +4016,7 @@ def PrintSampleParms(Sample): if Print: - pFile.write('\n Histogram: %s histogram Id: %d\n'%(histogram,hId)) - pFile.write(135*'='+'\n') + pFile.write(f'\n Histogram: {histogram} histogram Id: {hId}\n{135*"-"}\n') Units = {'C':' deg','T':' msec','B':' deg','E':'keV','A':' deg'} units = Units[controlDict[pfx+'histType'][2]] Limits = controlDict[pfx+'Limits'] @@ -4172,11 +4247,9 @@ def PrintSampleParmsSig(Sample,sampleSig): parmDict[pfx+'Scale'] = max(1.e-12,parmDict[pfx+'Scale']) #put floor on phase fraction scale sampSig = SetSampleParms(pfx,Sample,parmDict,sigDict) - if Print and not seq: - pFile.write('\n Histogram: %s histogram Id: %d\n'%(histogram,hId)) - pFile.write(135*'='+'\n') if Print: - pFile.write(' PWDR histogram weight factor = '+'%.3f\n'%(Histogram['wtFactor'])) + pFile.write(f'\n Histogram: {histogram} histogram Id: {hId}\n{135*"-"}\n') + pFile.write(f" PWDR histogram weight factor = {Histogram['wtFactor']:.3f}\n") pFile.write(' Final refinement wR = %.2f%% on %d observations in this histogram\n'% (Histogram['Residuals']['wR'],Histogram['Residuals']['Nobs'])) pFile.write(' Other residuals: R = %.2f%%, R-bkg = %.2f%%, wR-bkg = %.2f%% wRmin = %.2f%%\n'% diff --git a/GSASII/GSASIIstrMain.py b/GSASII/GSASIIstrMain.py index 26ea8f8d..90c6e958 100644 --- a/GSASII/GSASIIstrMain.py +++ b/GSASII/GSASIIstrMain.py @@ -180,6 +180,10 @@ def AllPrmDerivs(Controls,Histograms,Phases,restraintDict,rigidbodyDict, '''Computes the derivative of the fitting function (total Chi**2) with respect to every parameter in the parameter dictionary (parmDict) by applying shift below the parameter value as well as above. + For a standard sequential fit, only the first pattern in the + fit is used. This is called in GSASIIdataGUI.GSASII.OnDerivCalc, + which is used in the parameter impact computation. Grouped + sequential fits are not implmented. :returns: a dict with the derivatives keyed by variable number. Derivatives are a list with three values: evaluated over @@ -187,6 +191,9 @@ def AllPrmDerivs(Controls,Histograms,Phases,restraintDict,rigidbodyDict, variable and d is a small delta value chosen for that variable type. ''' import re + if Controls.get('Groups',{}).get('groupDict',{}): + print('AllPrmDerivs is not implemented for grouped fits') + return {} rms = lambda y: np.sqrt(np.mean(y**2)) G2mv.Map2Dict(parmDict,varyList) begin = time.time() @@ -262,7 +269,7 @@ def AllPrmDerivs(Controls,Histograms,Phases,restraintDict,rigidbodyDict, return derivCalcs def RefineCore(Controls,Histograms,Phases,restraintDict,rigidbodyDict,parmDict,histDict1,varyList, - calcControls,pawleyLookup,ifSeq,printFile,dlg,refPlotUpdate=None): + calcControls,pawleyLookup,ifSeq,printFile,dlg,refPlotUpdate=None,ifPrint=True): '''Core optimization routines, shared between SeqRefine and Refine :returns: 5-tuple of ifOk (bool), Rvals (dict), result, covMatrix, sig @@ -272,9 +279,7 @@ def RefineCore(Controls,Histograms,Phases,restraintDict,rigidbodyDict,parmDict,h # end patch # print 'current',varyList # for item in parmDict: print item,parmDict[item] ######### show dict just before refinement - ifPrint = True - if ifSeq: - ifPrint = False + Rvals = {} chisq0 = None Lastshft = None @@ -362,8 +367,8 @@ def RefineCore(Controls,Histograms,Phases,restraintDict,rigidbodyDict,parmDict,h Rvals['Nobs'] = Histograms['Nobs'] Rvals['Nvars'] = len(varyList) Rvals['RestraintSum'] = Histograms.get('RestraintSum',0.) - Rvals['Restraints'] = Histograms.get('Restraints',{}) - Rvals['nRestraints'] = Histograms.get('nRestraints',{}) + #Rvals['Restraints'] = Histograms.get('Restraints',{}) + #Rvals['nRestraints'] = Histograms.get('nRestraints',{}) Rvals['RestraintTerms'] = Histograms.get('RestraintTerms',0) Rvals['Rwp'] = np.sqrt(Rvals['chisq']/Histograms['sumwYo'])*100. #to % Rvals['GOF'] = np.sqrt(Rvals['chisq']/(Histograms['Nobs']+Rvals['RestraintTerms']-len(varyList))) @@ -759,9 +764,12 @@ def DoNoFit(GPXfile,key): return Histograms[key]['Data'][3] def DoLeBail(GPXfile,dlg=None,cycles=10,refPlotUpdate=None,seqList=None): - '''Fit LeBail intensities without changes to any other refined parameters. + '''Fit LeBail intensities without changes to any parameters flagged to be refined. This is a stripped-down version of :func:`Refine` that does not perform - any refinement cycles + any refinement cycles. + + Note that since no refinements are done, we can treat this sequential fits + as if simultaneous and process all histograms together. :param str GPXfile: G2 .gpx file name :param wx.ProgressDialog dlg: optional progress window to update. @@ -785,7 +793,11 @@ def DoLeBail(GPXfile,dlg=None,cycles=10,refPlotUpdate=None,seqList=None): constrDict,fixedList = G2stIO.ReadConstraints(GPXfile) restraintDict = {} Histograms_All,Phases = G2stIO.GetUsedHistogramsAndPhases(GPXfile) - if seqList: + if seqList and type(seqList) is dict: # grouped fit + Histograms = {} + for key in seqList: + Histograms.update({i:Histograms_All[i] for i in seqList[key]}) + elif seqList: Histograms = {i:Histograms_All[i] for i in seqList} else: Histograms = Histograms_All @@ -847,10 +859,16 @@ def DoLeBail(GPXfile,dlg=None,cycles=10,refPlotUpdate=None,seqList=None): print(traceback.format_exc()) return False,{'msg':Msg.msg} -def phaseCheck(phaseVary,Phases,histogram): - ''' - Removes unused parameters from phase varylist if phase not in histogram - for seq refinement removes vars in "Fix FXU" and "FixedSeqVars" here +def getPhaseVary(phaseVary,Phases,histogram): + '''Used in sequential refinements. Scans phases for variables that are refined, + if the phase is present & "use" is set for the selected histogram. + + Removes vars in "Fix FXU" and "FixedSeqVars" here + + :param list phaseVary: a list of phase variables generated in G2stIO.GetPhaseData + :param dict Phases: the Phase dict, but only including phases currently being used + :param str histogram: current histogram + :returns: variables to be used: copy of phaseVary with items removed ''' NewVary = [] for phase in Phases: @@ -874,16 +892,15 @@ def phaseCheck(phaseVary,Phases,histogram): def SeqRefine(GPXfile,dlg,refPlotUpdate=None): '''Perform a sequential refinement -- cycles through all selected histgrams, - one at a time + one at a time or in a group refinement, one group at a time ''' from . import GSASIImpsubs as G2mp G2mp.InitMP() # from . import pytexture as ptx ptx.pyqlmninit() #initialize fortran arrays for spherical harmonics msgs = {} + pflag = True printFile = open(ospath.splitext(GPXfile)[0]+'.lst','w') - G2fil.G2Print ('Starting Sequential Refinement') - G2stIO.ShowBanner(printFile) Controls = G2stIO.GetControls(GPXfile) preFrozenCount = 0 for h in Controls['parmFrozen']: @@ -893,6 +910,10 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): G2stIO.ShowControls(Controls,printFile,SeqRef=True,preFrozenCount=preFrozenCount) restraintDict = G2stIO.GetRestraints(GPXfile) Histograms,Phases = G2stIO.GetUsedHistogramsAndPhases(GPXfile) + histNames = Controls.get('Seq Data',[]) # group names or individual hists + groupDict = Controls.get('Groups',{}).get('groupDict',{}) + if groupDict: + histNames = list(groupDict.keys()) if not Phases: G2fil.G2Print (' *** ERROR - you have no phases to refine! ***') G2fil.G2Print (' *** Refine aborted ***') @@ -901,6 +922,12 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): G2fil.G2Print (' *** ERROR - you have no data to refine with! ***') G2fil.G2Print (' *** Refine aborted ***') return False,'No data' + if not histNames: + G2fil.G2Print (' *** ERROR - no sequential histograms/groups to fit! ***') + G2fil.G2Print (' *** Refine aborted ***') + return False,'Nothing selected' + G2fil.G2Print ('Starting Sequential Refinement') + G2stIO.ShowBanner(printFile) rigidbodyDict = G2stIO.GetRigidBodies(GPXfile) rbIds = rigidbodyDict.get('RBIds',{'Vector':[],'Residue':[]}) rbVary,rbDict = G2stIO.GetRigidBodyModels(rigidbodyDict,pFile=printFile) @@ -910,35 +937,55 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): for item in phaseVary: if '::A0' in item: G2fil.G2Print ('**** WARNING - lattice parameters should not be refined in a sequential refinement ****') - G2fil.G2Print ('**** instead use the Dij parameters for each powder histogram ****') + G2fil.G2Print ('**** instead use the Dij (hydrostatic strain) param for each powder hist ****') return False,'Lattice parameter refinement error - see console message' if '::C(' in item: G2fil.G2Print ('**** WARNING - phase texture parameters should not be refined in a sequential refinement ****') - G2fil.G2Print ('**** instead use the C(L,N) parameters for each powder histogram ****') + G2fil.G2Print ('**** instead use the C(L,N) parameters (in Phase/Data) for each powder hist ****') return False,'Phase texture refinement error - see console message' - if 'Seq Data' in Controls: - histNames = Controls['Seq Data'] - else: # patch from before Controls['Seq Data'] was implemented? - histNames = G2stIO.GetHistogramNames(GPXfile,['PWDR',]) if Controls.get('Reverse Seq'): - histNames.reverse() + histNames.reverse() SeqResult = G2stIO.GetSeqResult(GPXfile) # SeqResult = {'SeqPseudoVars':{},'SeqParFitEqList':[]} - Histo = {} + Histo = {} # histogram(s) in current group or only 1 if not grouped NewparmDict = {} G2stIO.SetupSeqSavePhases(GPXfile) msgs['steepestNum'] = 0 msgs['maxshift/sigma'] = [] - lasthist = '' - for ihst,histogram in enumerate(histNames): + lastHstLst = None # used to xfer Le Bail intensities from a previous seq fit to the current + for ihst,hg in enumerate(histNames): + # loop vars used here: + # hg: name of histogram or group + # hlist: list of histogram names (one or several) if GSASIIpath.GetConfigValue('Show_timing'): t1 = time.time() - G2fil.G2Print('\nRefining with '+str(histogram)) G2mv.InitVars() - (Natoms,atomIndx,phaseVary,phaseDict,pawleyLookup,FFtables,EFtables,ORBtables,BLtables,MFtables,maxSSwave) = \ - G2stIO.GetPhaseData(Phases,restraintDict,rbIds,Print=False,pFile=printFile,seqHistName=histogram) - ifPrint = False + pf1st = (pflag and ihst==0) # put phase info into .lst only on 1st histo/group (better would be to do this when a new phase is added, but...) + if groupDict: + #for key in groupDict.keys(): + word = f'{hg} group' + grEquivTbl,grHIDlist = G2stIO.groupEquivTbl(hg,groupDict,Histograms,warn=False) + hlist = groupDict[hg] + skip = False + for h in groupDict[hg]: + if h not in Histograms: + G2fil.G2Print(f"Error: {h} not found in group {hg}, skipping!") + #raise G2obj.G2Exception(f"refining with invalid histogram {h}") + skip = True + if skip: continue + (Natoms,atomIndx,phaseVary,phaseDict,pawleyLookup,FFtables,EFtables,ORBtables,BLtables,MFtables,maxSSwave + ) = G2stIO.GetPhaseData(Phases,restraintDict,rbIds,Print=pf1st,pFile=printFile,grHistList=groupDict[hg]) + else: + hlist = [hg] + if hg not in Histograms: + G2fil.G2Print(f"Error: histogram {hg} not found, skipping!") + #raise G2obj.G2Exception(f"refining with invalid histogram {hg}") + continue + word = f'{hg}' + (Natoms,atomIndx,phaseVary,phaseDict,pawleyLookup,FFtables,EFtables,ORBtables,BLtables,MFtables,maxSSwave + ) = G2stIO.GetPhaseData(Phases,restraintDict,rbIds,Print=pf1st,pFile=printFile,seqHistName=hg) + G2fil.G2Print(f'\nRefining with {word}') if dlg: - dlg.SetTitle('Residual for histogram '+str(ihst)) + dlg.SetTitle(f'Residual for {word} (#{ihst})') calcControls = {} calcControls['atomIndx'] = atomIndx calcControls['Natoms'] = Natoms @@ -948,17 +995,16 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): calcControls['BLtables'] = BLtables calcControls['MFtables'] = MFtables calcControls['maxSSwave'] = maxSSwave - if histogram not in Histograms: - G2fil.G2Print("Error: not found!") - raise G2obj.G2Exception("refining with invalid histogram {}".format(histogram)) - hId = Histograms[histogram]['hId'] - redphaseVary = phaseCheck(phaseVary,Phases,histogram) - Histo = {histogram:Histograms[histogram],} + hPhaseVary = [] + for h in hlist: + hPhaseVary += getPhaseVary(phaseVary,Phases,h) + hPhaseVary = list(set(hPhaseVary)) + Histo = {h:Histograms[h] for h in hlist} hapVary,hapDict,controlDict = G2stIO.GetHistogramPhaseData(Phases,Histo,Controls=calcControls,Print=False) calcControls.update(controlDict) histVary,histDict,histDict1, controlDict = G2stIO.GetHistogramData(Histo,False) calcControls.update(controlDict) - varyList = rbVary+redphaseVary+hapVary+histVary + varyList = rbVary+hPhaseVary+hapVary+histVary # if not ihst: # save the initial vary list, but without histogram numbers on parameters saveVaryList = varyList[:] @@ -977,31 +1023,56 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): parmDict.update(phaseDict) parmDict.update(hapDict) parmDict.update(histDict) - if Controls['Copy2Next']: # update with parms from last histogram - #parmDict.update(NewparmDict) # don't use in case extra entries would cause a problem - for parm in NewparmDict: - if parm in parmDict: - parmDict[parm] = NewparmDict[parm] - for phase in Phases: - if Phases[phase]['Histograms'][histogram].get('LeBail',False) and lasthist: - oldFsqs = Histograms[lasthist]['Reflection Lists'][phase]['RefList'].T[8:10] #assume no superlattice! - newRefs = Histograms[histogram]['Reflection Lists'][phase]['RefList'] - if len(newRefs) == len(oldFsqs.T): - newRefs.T[8:10] = copy.copy(oldFsqs) - # for i,ref in enumerate(newRefs): - # ref[8:10] = oldFsqs.T[i] - else: - print('ERROR - mismatch in reflection list length bewteen %s and %s; no copy done'%(lasthist,histogram)) -####TBD: if LeBail copy reflections here? - elif histogram in SeqResult: # update phase from last seq ref - NewparmDict = SeqResult[histogram].get('parmDict',{}) + # update with parms from last histogram + if Controls['Copy2Next']: + # update parmDict with NewparmDict, but only with entries that are already present + #for parm in NewparmDict: + # if parm in parmDict: + # parmDict[parm] = NewparmDict[parm] + parmDict.update({parm:NewparmDict[parm] for parm in NewparmDict if parm in parmDict}) + + if lastHstLst and len(hlist) != len(lastHstLst): + stoploop = False + for phase in Phases: + if stoploop: break + for h in hlist: + if Phases[phase]['Histograms'][h].get('LeBail',False): + print(f'LeBail update skipped for group {hg} - group length changed') + stoploop = True + break + elif lastHstLst: + for phase in Phases: + for h,ph in zip(hlist,lastHstLst): + Phases[phase]['Histograms'][h]['LeBail'] = Phases[phase]['Histograms'][h].get('LeBail',False) + if Phases[phase]['Histograms'][h]['LeBail']: + # copy LeBail intensities from previous histogram + oldFsqs = Histograms[ph]['Reflection Lists'][phase]['RefList'].T[8:10] #assume no superlattice! + newRefs = Histograms[h]['Reflection Lists'][phase]['RefList'] + if len(newRefs) == len(oldFsqs.T): + newRefs.T[8:10] = copy.copy(oldFsqs) + # for i,ref in enumerate(newRefs): + # ref[8:10] = oldFsqs.T[i] + else: + print(f'LeBail update problem - mismatch in reflection list length between histograms {ph} and {h}; no copy done') + elif hg in SeqResult: # update phase from last seq ref + # I'm not 100% sure what this next code is doing. It seems to be loading + # previous parameters from the sequential table for use in the next fit + # but why is unclear. Perhaps this is from when the sequential + # results were not saved in the main data sections. + NewparmDict = SeqResult[hg].get('parmDict',{}) for parm in NewparmDict: if '::' in parm and parm in parmDict: parmDict[parm] = NewparmDict[parm] G2stIO.GetFprime(calcControls,Histo) - # do constraint processing (again, if called from GSASIIdataGUI.GSASII.OnSeqRefine) - constrDict,fixedList = G2stIO.ReadConstraints(GPXfile,seqHist=hId) + # do constraint processing for current histogram. This is a repeat when + # called from GSASIIdataGUI.GSASII.OnSeqRefine but this time the results are needed + if groupDict: + constrDict,fixedList = G2stIO.ReadConstraints(GPXfile, + grEquivTbl=grEquivTbl,grHIDlist=grHIDlist) + else: + constrDict,fixedList = G2stIO.ReadConstraints(GPXfile, + seqHist=Histograms[hg]['hId']) varyListStart = tuple(varyList) # save the original varyList before dependent vars are removed msg = G2mv.EvaluateMultipliers(constrDict,phaseDict,hapDict,histDict) @@ -1009,14 +1080,18 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): return False,'Unable to interpret multiplier(s): '+msg try: - errmsg,warnmsg,groups,parmlist = G2mv.GenerateConstraints(varyList,constrDict,fixedList,parmDict, - seqHistNum=hId,raiseException=True) + if groupDict: + errmsg,warnmsg,groups,parmlist = G2mv.GenerateConstraints(varyList,constrDict,fixedList,parmDict, + seqHistNum=None,grEquivTbl=grEquivTbl,grHIDlist=grHIDlist,raiseException=True) + else: + errmsg,warnmsg,groups,parmlist = G2mv.GenerateConstraints(varyList,constrDict,fixedList,parmDict, + seqHistNum=Histograms[hg]['hId'],raiseException=True) constraintInfo = (groups,parmlist,constrDict,fixedList,ihst) G2mv.normParms(parmDict) G2mv.Map2Dict(parmDict,varyList) # changes varyList except G2mv.ConstraintException: - G2fil.G2Print (' *** ERROR - your constraints are internally inconsistent for histogram {}***'.format(hId)) - return False,' Constraint error' + G2fil.G2Print(f'*** ERROR - your constraints are internally inconsistent for hist/group #{ihst} ({hg}) ***') + return False,' Constraint error, see concole' if not ihst: # first histogram to refine against firstVaryList = [] @@ -1036,10 +1111,7 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): item = ':'.join(items) newVaryList.append(item) if newVaryList != firstVaryList and Controls['Copy2Next']: - # variable lists are expected to match between sequential refinements when Copy2Next is on - #print '**** ERROR - variable list for this histogram does not match previous' - #print ' Copy of variables is not possible' - #print '\ncurrent histogram',histogram,'has',len(newVaryList),'variables' + # warn when variable list for this histogram does not match previous with copy2next combined = list(set(firstVaryList+newVaryList)) c = [var for var in combined if var not in newVaryList] p = [var for var in combined if var not in firstVaryList] @@ -1058,16 +1130,14 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): firstVaryList = newVaryList ifSeq = True - printFile.write('\n Refinement results for histogram id {}: {}\n' - .format(hId,histogram)) - printFile.write(135*'-'+'\n') - lasthist = histogram + printFile.write(f'\n{135*"="}\nRefinement results for {word}\n{135*"="}\n') + lastHstLst = hlist # remove frozen vars - if 'parmFrozen' not in Controls: - Controls['parmFrozen'] = {} - if histogram not in Controls['parmFrozen']: - Controls['parmFrozen'][histogram] = [] - parmFrozenList = Controls['parmFrozen'][histogram] + parmFrozenList = [] + if 'parmFrozen' not in Controls: Controls['parmFrozen'] = {} + for h in hlist: + if h not in Controls['parmFrozen']: Controls['parmFrozen'][h] = [] + parmFrozenList += Controls['parmFrozen'][h] frozenList = [i for i in varyList if i in parmFrozenList] if len(frozenList) != 0: varyList = [i for i in varyList if i not in parmFrozenList] @@ -1083,7 +1153,7 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): try: IfOK,Rvals,result,covMatrix,sig,Lastshft = RefineCore(Controls,Histo,Phases,restraintDict, rigidbodyDict,parmDict,histDict1,varyList,calcControls,pawleyLookup,ifSeq,printFile,dlg, - refPlotUpdate=refPlotUpdate) + refPlotUpdate=refPlotUpdate,ifPrint=pflag) try: shft = '%.4f'% Rvals['Max shft/sig'] except: @@ -1096,7 +1166,7 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): msgs['maxshift/sigma'].append(Rvals['Max shft/sig']) # add the uncertainties into the esd dictionary (sigDict) if not IfOK: - G2fil.G2Print('***** Sequential refinement failed at histogram '+histogram,mode='warn') + G2fil.G2Print(f'***** Sequential refinement failed at histogram/group {hg}',mode='warn') break sigDict = dict(zip(varyList,sig)) # add indirectly computed uncertainties into the esd dict @@ -1104,21 +1174,21 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): newCellDict = copy.deepcopy(G2stMth.GetNewCellParms(parmDict,varyList)) newAtomDict = copy.deepcopy(G2stMth.ApplyXYZshifts(parmDict,varyList)) - SeqResult[histogram] = { + SeqResult[hg] = { 'variables':result[0],'varyList':varyList,'sig':sig,'Rvals':Rvals, 'varyListStart':varyListStart,'Lastshft':Lastshft, - 'covMatrix':covMatrix,'title':histogram,'newAtomDict':newAtomDict, + 'covMatrix':covMatrix,'title':hg,'newAtomDict':newAtomDict, 'newCellDict':newCellDict,'depParmDict':{}, 'constraintInfo':constraintInfo, 'parmDict':parmDict, } G2stMth.ApplyRBModels(parmDict,Phases,rigidbodyDict,True) - SeqResult[histogram]['RBsuDict'] = G2stMth.computeRBsu(parmDict,Phases,rigidbodyDict, + SeqResult[hg]['RBsuDict'] = G2stMth.computeRBsu(parmDict,Phases,rigidbodyDict, covMatrix,varyList,sig) G2stIO.SetISOmodes(parmDict,sigDict,Phases,None) - G2stIO.SetHistogramPhaseData(parmDict,sigDict,Phases,Histo,None,ifPrint, + G2stIO.SetHistogramPhaseData(parmDict,sigDict,Phases,Histo,None,pflag, pFile=printFile,covMatrix=covMatrix,varyList=varyList) - G2stIO.SetHistogramData(parmDict,sigDict,Histo,None,ifPrint,printFile,seq=True) + G2stIO.SetHistogramData(parmDict,sigDict,Histo,None,pflag,printFile,seq=True) # check for variables outside their allowed range, reset and freeze them frozen = dropOOBvars(varyList,parmDict,sigDict,Controls,parmFrozenList) msg = None @@ -1136,46 +1206,61 @@ def SeqRefine(GPXfile,dlg,refPlotUpdate=None): result[0][i] = parmDict[p] sig[i] = -0.1 # a dict with values & esds for dependent (constrained) parameters - avoid extraneous holds - SeqResult[histogram]['depParmDict'] = {i:(parmDict[i],sigDict[i]) for i in sigDict if i not in varyList} + SeqResult[hg]['depParmDict'] = {i:(parmDict[i],sigDict[i]) for i in sigDict if i not in varyList} G2stIO.SaveUpdatedHistogramsAndPhases(GPXfile,Histo,Phases, - rigidbodyDict,SeqResult[histogram],Controls['parmFrozen']) + rigidbodyDict,SeqResult[hg],Controls['parmFrozen']) if msg: printFile.write(msg+'\n') NewparmDict = {} # make dict of varied parameters in current histogram, renamed to # next histogram, for use in next refinement. if Controls['Copy2Next'] and ihst < len(histNames)-1: - hId = Histo[histogram]['hId'] # current histogram - nexthId = Histograms[histNames[ihst+1]]['hId'] + nexthg = histNames[ihst+1] # next histogram/group in list + if groupDict: + hIdlist = [str(Histograms[h]['hId']) for h in groupDict[hg]] + nexthIdlist = [str(Histograms[h]['hId']) for h in groupDict[nexthg]] + else: + hIdlist = [str(Histo[hg]['hId']),] + nexthIdlist = [str(Histo[nexthg]['hId']),] + replDict = dict(zip(hIdlist,nexthIdlist)) for parm in set(list(varyList)+list(varyListStart)): + # copy over atom positions (when refined) not delta values + if items[2].startswith('dA'): parm = parm.replace(':dA',':A') + nextparm = parm items = parm.split(':') - if len(items) < 3: - continue - if str(hId) in items[1]: - items[1] = str(nexthId) - newparm = ':'.join(items) - NewparmDict[newparm] = parmDict[parm] - else: - if items[2].startswith('dA'): parm = parm.replace(':dA',':A') - NewparmDict[parm] = parmDict[parm] + if len(items) >= 3: + # replace histograms in current histogram with ones in next + items[1] = replDict.get(items[1],items[1]) + nextparm = ':'.join(items) + NewparmDict[nextparm] = parmDict[parm] except G2obj.G2RefineCancel as Msg: if not hasattr(Msg,'msg'): Msg.msg = str(Msg) printFile.close() G2fil.G2Print (' ***** Refinement stopped *****') + if GSASIIpath.GetConfigValue('debug'): + import traceback + print(traceback.format_exc()) return False,Msg.msg except (G2obj.G2Exception,Exception) as Msg: # cell metric error, others? if not hasattr(Msg,'msg'): Msg.msg = str(Msg) printFile.close() G2fil.G2Print (' ***** Refinement error *****') + if GSASIIpath.GetConfigValue('debug'): + import traceback + print(traceback.format_exc()) return False,Msg.msg if GSASIIpath.GetConfigValue('Show_timing'): t2 = time.time() G2fil.G2Print("Fit step time {:.2f} sec.".format(t2-t1)) t1 = t2 - SeqResult['histNames'] = [itm for itm in G2stIO.GetHistogramNames(GPXfile,['PWDR',]) if itm in SeqResult.keys()] + if groupDict: + entries = sorted(set(SeqResult.get('histNames',[])+histNames)) + SeqResult['histNames'] = [i for i in entries if i in SeqResult] + else: + SeqResult['histNames'] = [itm for itm in G2stIO.GetHistogramNames(GPXfile,['PWDR',]) if itm in SeqResult.keys()] try: G2stIO.SetSeqResult(GPXfile,Histograms,SeqResult) except Exception as msg: @@ -1584,7 +1669,9 @@ def ShowBanner(name): G2fil.G2Print ('\n Best plane RMS X =%8.3f, Y =%8.3f, Z =%8.3f'%(Evec[Order[2]],Evec[Order[1]],Evec[Order[0]])) def do_refine(*args): - 'Called to run a refinement when this module is executed ' + '''Called to run a refinement when this module is run directly rather than + imported. + ''' starttime = time.time() #arg = sys.argv if len(args) >= 1: @@ -1592,23 +1679,33 @@ def do_refine(*args): elif len(sys.argv) > 1: files = sys.argv[1:] else: - G2fil.G2Print ('ERROR GSASIIstrMain.do_refine error - missing filename') - G2fil.G2Print ('Use "python GSASIIstrMain.py f1.gpx [f2.gpx f3.gpx...]" to run') - G2fil.G2Print ('or call GSASIIstrMain.do_refine directly') + G2fil.G2Print ('ERROR GSASIIstrMain.do_refine: missing .gpx file to use') sys.exit() for GPXfile in files: if not ospath.exists(GPXfile): - G2fil.G2Print ('ERROR - '+GPXfile+" doesn't exist! Skipping.") + G2fil.G2Print (f"ERROR - {GPXfile} doesn't exist! Skipping.") continue - # TODO: test below # figure out if this is a sequential refinement and call SeqRefine(GPXfile,None) - #Controls = G2stIO.GetControls(GPXfile) - #if Controls.get('Seq Data',[]): + Controls = G2stIO.GetControls(GPXfile) + if Controls.get('Seq Data',[]) or Controls.get('Groups',[]): + SeqRefine(GPXfile,None) + else: Refine(GPXfile,None) - #else: - # SeqRefine(GPXfile,None) G2fil.G2Print("Done with {}.\nExecution time {:.2f} sec.".format(GPXfile,time.time()-starttime)) if __name__ == '__main__': + '''used to refine with a .gpx file, bypassing the GUI. To do this, if GSAS-II + is not installed inside Python, one must deal with the python path. The easiest + way to do this is to create a file with commands like this:: + + import runpy + import sys + sys.argv[1:] = ['/Users/toby/Scratch/GroupPWDR/2Px4G_copy2test.gpx'] + runpy.run_module("GSASII.GSASIIstrMain", run_name="__main__") + + put that file inside the parent directory to GSASII and run it from there. + + Consider using GSASIIscriptable rather than this. + ''' GSASIIpath.InvokeDebugOpts() do_refine() diff --git a/GSASII/exports/G2export_CIF.py b/GSASII/exports/G2export_CIF.py index 4d24f289..f1c84a07 100644 --- a/GSASII/exports/G2export_CIF.py +++ b/GSASII/exports/G2export_CIF.py @@ -4059,7 +4059,7 @@ def SelectDisAglFlags(event): #=============================================================================== seqmode = False seqHistList = [] - if self.G2frame.testSeqRefineMode(): + if bool(self.G2frame.testSeqRefineMode(True)): if self.seqData is None: raise Exception('Use Export/Sequential project for sequential refinements') if len(self.Phases) > 1: @@ -4080,7 +4080,7 @@ def SelectDisAglFlags(event): return missing = [] oldref = 0 - for h in self.G2frame.testSeqRefineMode(): + for h in self.G2frame.testSeqRefineMode(True): # TODO needs work for Grouped Seq (groupDict) if h not in seqHistList: missing.append(h) for h in seqHistList: diff --git a/GSASII/meson.build b/GSASII/meson.build index 49163069..650740bb 100644 --- a/GSASII/meson.build +++ b/GSASII/meson.build @@ -9,7 +9,6 @@ py.install_sources([ 'GSASIIGUI.py', 'GSASIIElem.py', 'GSASIIElemGUI.py', - # 'GSASIIIO.py', 'GSASIImiscGUI.py', 'GSASIIIntPDFtool.py', 'GSASIIconstrGUI.py', @@ -20,11 +19,11 @@ py.install_sources([ 'GSASIIexprGUI.py', 'GSASIIfiles.py', 'GSASIIfpaGUI.py', + 'GSASIIgroupGUI.py', 'GSASIIimage.py', 'GSASIIimgGUI.py', 'GSASIIindex.py', 'GSASIIlattice.py', - # 'GSASIIlog.py', 'GSASIImapvars.py', 'GSASIImath.py', 'GSASIImpsubs.py', diff --git a/docs/source/objvarorg.rst b/docs/source/objvarorg.rst index 924fd08b..a0160998 100644 --- a/docs/source/objvarorg.rst +++ b/docs/source/objvarorg.rst @@ -129,22 +129,34 @@ _OffsetVals A list of offset values (as floats) that will be modes, currently. =========== ==================================================== -For `_seqmode` the meaning of the settings for constrained HAP and -histogram parameters is: - - * 'auto-wildcard' (Set hist # to \*): Any constraint specified with - a specific histogram number will be changed to apply to each - histogram as it is processed. +The `_seqmode` is used for sequential fits and the meaning of the +settings for constrained HAP and histogram parameters is: + + * 'auto-wildcard' (Set hist # to \*) + Any constraint that has a specific histogram number will be + changed to apply to each histogram as it is processed. + Note that for grouped sequential fits, a table of "equivalent + histograms" is created and any histogram number that matches + will be converted to a histogram number in the current group. + Also for grouped sequential fits, wildcards used in equivalences + will be processed as described as in 'wildcards-only' mode, below. - * 'wildcards-only' (Ignore unless hist=\*): Only constraints - specified with a wildcard for the histogram will be - used and will be applied to each histogram as it is - processed. Contraints that include a specific histogram number - will be ignored. - - * 'use-all' (Use as supplied): This uses both wildcard histograms + * 'wildcards-only' (Ignore unless hist=\*) + Only constraints + specified with a wildcard for the histogram will be + used and will be applied to each histogram as it is + processed. Contraints that include a specific histogram number + will be ignored. + For grouped sequential fits, wildcards only make sense for + equivalences and for them the constraint will be expanded to + include all histograms in the current group. Thus, a constraint on + `p:*:D11` will force the reciprocal cell offset to be the same for + all histograms in a group. + + * 'use-all' (Use as supplied) + This uses both wildcard histograms and those where specific histograms are specified. Wildcards are - applied to each histogram as it is processed. Contraints that + applied to each histogram as it is processed. Constraints that include a specific histogram number are used only for that histogram. .. _Constraint_definitions_table: diff --git a/testModule.py b/testModule.py new file mode 100644 index 00000000..4cdb3b5b --- /dev/null +++ b/testModule.py @@ -0,0 +1,15 @@ +# This is an example for how to run a GSAS-II module from the command +# line. This file must be in the same location as the GSASII directory +# or GSASII must be installed as a Python module. + +# Here the arguments array is "faked out" so this emulates running +# `python GSASII/GSASIIstrMain.py /Users/.../2Px4G_copy2test.gpx` +# (which does not work due to import problems). + +# Note that args init_globals and alter_sys might be useful to +# trigger some special actions. + +import runpy +import sys +sys.argv[1:] = ['/Users/toby/Scratch/GroupPWDR/2Px4G_copy2test.gpx'] +runpy.run_module("GSASII.GSASIIstrMain", run_name="__main__")