diff --git a/GSASII/GSASIIdataGUI.py b/GSASII/GSASIIdataGUI.py index d0a2d107..afdddc15 100644 --- a/GSASII/GSASIIdataGUI.py +++ b/GSASII/GSASIIdataGUI.py @@ -67,6 +67,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 @@ -1404,6 +1405,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 @@ -1997,7 +1999,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 @@ -4300,13 +4302,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) @@ -4326,6 +4329,8 @@ def StartProject(self): seqId = item elif name == "Phases": phaseId = item + elif name.startswith("Groups"): + GroupId = item elif name == 'Controls': data = self.GPXtree.GetItemPyData(item) if data: @@ -4333,19 +4338,27 @@ def StartProject(self): item, cookie = self.GPXtree.GetNextChild(self.root, cookie) if phaseId: # show all phases self.GPXtree.Expand(phaseId) - if seqId: + if GroupId: + self.GPXtree.Expand(GroupId) + # select an item + if seqId and selectItem: 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: + 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: 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: + elif phaseId and selectItem: 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() @@ -5524,7 +5537,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 @@ -7496,6 +7519,20 @@ 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','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_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): @@ -7902,26 +7939,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: @@ -7962,11 +8036,44 @@ def ClearFrozen(event): 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(wx.StaticText(G2frame.dataWindow,label='Histogram Grouping'),0,WACV) + subSizer.Add((-1,-1),1,wx.EXPAND) + mainSizer.Add(subSizer,0,wx.EXPAND) + subSizer = wx.BoxSizer(wx.HORIZONTAL) + groupDict = data.get('Groups',{}).get('groupDict',{}) + subSizer.Add((-1,-1),1,wx.EXPAND) + if groupDict: + 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" + subSizer.Add(wx.StaticText(G2frame.dataWindow,label=msg),0,WACV) + subSizer.Add((5,-1)) + btn = wx.Button(G2frame.dataWindow, wx.ID_ANY,'Redefine groupings') + else: + btn = wx.Button(G2frame.dataWindow, wx.ID_ANY,'Define groupings') + btn.Bind(wx.EVT_BUTTON,SearchGroups) + subSizer.Add(btn) + if groupDict: + btn = wx.Button(G2frame.dataWindow, wx.ID_ANY,'Clear groupings') + subSizer.Add((5,-1)) + subSizer.Add(btn) + btn.Bind(wx.EVT_BUTTON,ClearGroups) subSizer.Add((-1,-1),1,wx.EXPAND) mainSizer.Add(subSizer,0,wx.EXPAND) - mainSizer.Add(AuthSizer()) - mainSizer.Add((5,5),0) + mainSizer.Add((-1,8)) + 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()) Controls = data # count frozen variables (in appropriate place) for key in ('parmMinDict','parmMaxDict','parmFrozen'): @@ -8889,6 +8996,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)) ############################################################################ @@ -9089,6 +9201,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) diff --git a/GSASII/GSASIIfiles.py b/GSASII/GSASIIfiles.py index 231a34bb..e8e6f976 100644 --- a/GSASII/GSASIIfiles.py +++ b/GSASII/GSASIIfiles.py @@ -1467,7 +1467,11 @@ def FormatValue(val,maxdigits=None): digits.append('f') if not val: digits[2] = 'f' - fmt="{:"+str(digits[0])+"."+str(digits[1])+digits[2]+"}" + if digits[2] == 'g': + fmt="{:#"+str(digits[0])+"."+str(digits[1])+digits[2]+"}" + # the # above forces inclusion of a decimal place: 10.000 rather than 10 for 9.999999999 + else: + fmt="{:"+str(digits[0])+"."+str(digits[1])+digits[2]+"}" string = fmt.format(float(val)).strip() # will standard .f formatting work? if len(string) <= digits[0]: if ':' in string: # deal with weird bug where a colon pops up in a number when formatting (EPD 7.3.2!) diff --git a/GSASII/GSASIIgroupGUI.py b/GSASII/GSASIIgroupGUI.py new file mode 100644 index 00000000..44f68de8 --- /dev/null +++ b/GSASII/GSASIIgroupGUI.py @@ -0,0 +1,1122 @@ +# -*- coding: utf-8 -*- +''' +Routines for working with groups of histograms. + +Groups are defined in Controls entry ['Groups'] which contains three entries: + +* 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 + +** Parameter Data Table ** + +For use to create GUI tables and to copy values between histograms, parameters +are organized in a dict where each dict entry has contents of form: + + * dict['__dataSource'] : SourceArray + + * dict[histogram]['label'] : `innerdict` + +where `label` is the text shown on the row label and `innerdict` can contain +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) + +One of 'val', 'ref' or 'str' elements will 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. + + * 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 must be + edited in the histogram section. + + * The 'fmt' value is a string used to format the 'str' value to + convert it to a string, if it is a float or int value. + + * 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. + +''' + +# import math +# import os +import re +# import copy +# import platform +# import pickle +# import sys +# import random as ran + +#import numpy as np +# import numpy.ma as ma +import wx + +# from . import GSASIIpath +from . import GSASIIdataGUI as G2gd +# from . import GSASIIobj as G2obj +# from . import GSASIIpwdGUI as G2pdG +# from . import GSASIIimgGUI as G2imG +# from . import GSASIIElem as G2el +# from . import GSASIIfiles as G2fil +# from . import GSASIIctrlGUI as G2G +# from . import GSASIImath as G2mth +# from . import GSASIIElem as G2elem +from . import GSASIIspc as G2spc +from . import GSASIIlattice as G2lat +# from . import GSASIIpwd as G2pwd +from . import GSASIIctrlGUI as G2G +from . import GSASIIpwdplot as G2pwpl +WACV = wx.ALIGN_CENTER_VERTICAL + +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,item,plot=True): + def onDisplaySel(event): + G2frame.GroupInfo['displayMode'] = dsplType.GetValue() + wx.CallAfter(UpdateGroup,G2frame,item,False) + + def copyPrep(): + 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): + 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]) + except Exception as msg: # could hit an error if an array element is not defined + pass + #print(msg) + #print('error with',i,dst) + def OnCopySel(event): + 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]) + except Exception as msg: # could hit an error if an array element is not defined + pass + #print(msg) + #print('error with',i,dst) + + Histograms,Phases = G2frame.GetUsedHistogramsAndPhasesfromTree() + if not hasattr(G2frame,'GroupInfo'): + G2frame.GroupInfo = {} + G2frame.GroupInfo['displayMode'] = G2frame.GroupInfo.get('displayMode','Sample') + G2frame.GroupInfo['groupName'] = G2frame.GPXtree.GetItemText(item) + 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) + + 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) + topSizer.Add(wx.StaticText(topParent, + label=f' parameters for group "{histLabels(G2frame)[0]}"'), + 0,WACV) + topSizer.Add((-1,-1),1,wx.EXPAND) + topSizer.Add(G2G.HelpButton(topParent,helpIndex=G2frame.dataWindow.helpKey)) + + if G2frame.GroupInfo['displayMode'].startswith('Hist'): + HAPframe(G2frame,Histograms,Phases) + else: + HistFrame(G2frame,Histograms) + if plot: G2pwpl.PlotPatterns(G2frame,plotType='GROUP') + G2frame.dataWindow.SetDataSize() + #wx.CallLater(100,G2frame.SendSizeEvent) + 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): + indx = arrayIndices[-1] + arr = dataSource[hist] + for i in arrayIndices[:-1]: + arr = arr[i] + return arr,indx + +def indexArrayVal(dataSource,hist,arrayIndices): + if arrayIndices is None: return None + arr = dataSource[hist] + for i in arrayIndices: + arr = arr[i] + return arr + +ClearAllLbl = '☐' # previously 'C' +SetAllLbl = '☑' # previously 'S' +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 edit widgets + ''' + but = event.GetEventObject() + dataSource = but.valDict['dataSource'] + valList = but.valDict['arrays'] + histList = but.valDict['hists'] + valEditList = but.valDict['valEditList'] + firstVal = indexArrayVal(dataSource,histList[0],valList[0]) + for c in valEditList: + c.ChangeValue(firstVal) + +def displayDataArray(rowLabels,DataArray,Sizer,Panel,lblRow=False,deltaMode=False, + lblSizer=None,lblPanel=None,CopyCtrl=True): + '''Displays the data table in `DataArray` in Scrolledpanel `Panel` + with wx.FlexGridSizer `Sizer`. + ''' + firstentry = None + #lblRow = True + if lblSizer is None: lblSizer = Sizer + if lblPanel is None: lblPanel = Panel + checkButList = {} + valEditList = {} + lblDict = {} + dataSource = DataArray['_dataSource'] + for row in rowLabels: + checkButList[row] = [] + valEditList[row] = [] + # show the row labels, when not in a separate sizer + if lblRow: + # is a copy across and/or a refine all button needed? + 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(lblPanel,label=row) + else: # used for "renameable" sample vars (str) + w = G2G.ValidatedTxtCtrl(lblPanel,arr,key,size=(125,-1)) + lblSizer.Add(w,0,wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_RIGHT) + lblDict[row] = w + + if len(refList) > 2: + lbl = SetAllLbl + if all([indexArrayVal(dataSource,hist,i) for i in refList]): lbl = ClearAllLbl + refAll = wx.Button(lblPanel,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] + lblSizer.Add(refAll,0,wx.ALIGN_CENTER_VERTICAL) + refAll.Bind(wx.EVT_BUTTON,onRefineAll) + else: + lblSizer.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: + but = wx.Button(Panel,wx.ID_ANY,'\u2192',style=wx.BU_EXACTFIT) + but.valDict = {'arrays': valList, 'hists': histList, + 'dataSource':dataSource, + 'valEditList' :valEditList[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]): + 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: + # does this ever happen? + 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 'val' in DataArray[hist][row] and 'ref' in DataArray[hist][row]: + valrefsiz = wx.BoxSizer(wx.HORIZONTAL) + arr,indx = indexArrayRef(dataSource,hist,DataArray[hist][row]['val']) + w = G2G.ValidatedTxtCtrl(Panel,arr,indx,size=(80,-1), + nDig=[9,7,'g'], + xmin=minval,xmax=maxval) + valEditList[row].append(w) + valrefsiz.Add(w,0,WACV) + if firstentry is None: firstentry = w + 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) + elif 'val' in DataArray[hist][row]: + 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) + valEditList[row].append(w) + Sizer.Add(w,0,wx.ALIGN_CENTER_VERTICAL|wx.ALIGN_LEFT) + if firstentry is None: firstentry = w + + elif 'ref' in DataArray[hist][row]: + 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]: + 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): + '''Put everything in a single FlexGridSizer. + ''' + #--------------------------------------------------------------------- + # generate a dict with values for each histogram + CopyCtrl = True + global prmArray + 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 = False + 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 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 + # remove rows where delta-terms are all zeros + if '\u0394' in G2frame.GroupInfo['displayMode']: + rowLabels = [i for i in rowLabels if i in nonZeroRows] + #======= Generate GUI =============================================== + # layout the window + panel = midPanel = G2frame.dataWindow + mainSizer = wx.BoxSizer(wx.VERTICAL) + G2G.HorizontalLine(mainSizer,panel) + panel.SetSizer(mainSizer) + deltaMode = "\u0394" in G2frame.GroupInfo['displayMode'] + n = 2 + if CopyCtrl and len(prmArray) > 2: n += 1 # add column for copy (when more than one histogram) + valSizer = wx.FlexGridSizer(0,len(prmArray)+n-1,3,10) + mainSizer.Add(valSizer,1,wx.EXPAND) + valSizer.Add(wx.StaticText(midPanel,label=' ')) + 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) + firstentry,lblDict = displayDataArray(rowLabels,prmArray,valSizer,midPanel, + lblRow=True, + 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 + +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]: + 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),} + # 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,]] + else: + parms += [['DisplaceX',u'Sample X displ',None,], + ['DisplaceY','Sample Y displ',None,], + ['Absorption','Sample abs,\xb5\xb7r',None,]] + elif dataType == 'Bragg-Brentano': + parms += [['Shift','Sample displ',None,], + ['Transparency','Sample transp',None], + ['SurfRoughA','Surf rough A',None], + ['SurfRoughB','Surf rough B',None]] + #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 key,lbl,fmt in parms: + 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),} + + 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]: + 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),} + 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]: + indexDict[hist] = {} + for lbl,indx,typ in [('Function',0,'str'), + ('ref flag',1,'ref'), + ('# Bkg terms',2,'str')]: + indexDict[hist][lbl] = { + typ : ('Background',0,indx) + } + if indx == 2: + indexDict[hist][lbl]['fmt'] = '.0f' + 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)} + 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)} + 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} + return indexDict + +def HAPframe(G2frame,Histograms,Phases): + '''This creates two side-by-side scrolled panels, each containing + a FlexGridSizer. + The panel to the left contains the labels for the sizer to the right. + This way the labels are not scrolled horizontally and are always seen. + The two vertical scroll bars are linked together so that the labels + are synced to the table of values. + ''' + def selectPhase(event): + 'Display the selected phase' + def OnScroll(event): + 'Synchronize vertical scrolling between the two scrolled windows' + obj = event.GetEventObject() + pos = obj.GetViewStart()[1] + if obj == lblScroll: + HAPScroll.Scroll(-1, pos) + else: + lblScroll.Scroll(-1, pos) + event.Skip() + #--------------------------------------------------------------------- + # selectPhase starts here. Find which phase is selected. + if event: + page = event.GetSelection() + #print('page selected',page,phaseList[page]) + else: # initial call when window is created + page = 0 + #print('no page selected',phaseList[page]) + # generate a dict with HAP values for each phase (may not be the same) + global prmArray + prmArray = getHAPvals(G2frame,phaseList[page],Histograms,Phases) + # construct a list of row labels, attempting to keep the + # order they appear in the original array + rowLabels = [] + lpos = 0 + for hist in prmArray: + if hist == '_dataSource': continue + prevkey = None + for key in prmArray[hist]: + 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 + #======= Generate GUI =============================================== + for panel in HAPtabs: + if panel.GetSizer(): + panel.GetSizer().Destroy() # clear out old widgets + panel = HAPtabs[page] + bigSizer = wx.BoxSizer(wx.HORIZONTAL) + panel.SetSizer(bigSizer) + # panel for labels; show scroll bars to hold the space + lblScroll = wx.lib.scrolledpanel.ScrolledPanel(panel, + style=wx.VSCROLL|wx.HSCROLL|wx.ALWAYS_SHOW_SB) + hpad = 3 # space between rows + lblSizer = wx.FlexGridSizer(0,2,hpad,2) + lblScroll.SetSizer(lblSizer) + bigSizer.Add(lblScroll,0,wx.EXPAND) + + # Create scrolled panel to display HAP data + HAPScroll = wx.lib.scrolledpanel.ScrolledPanel(panel, + style=wx.VSCROLL|wx.HSCROLL|wx.ALWAYS_SHOW_SB) + HAPSizer = wx.FlexGridSizer(0,len(prmArray),hpad,10) + HAPScroll.SetSizer(HAPSizer) + bigSizer.Add(HAPScroll,1,wx.EXPAND) + + # Bind scroll events to synchronize scrolling + lblScroll.Bind(wx.EVT_SCROLLWIN, OnScroll) + HAPScroll.Bind(wx.EVT_SCROLLWIN, OnScroll) + # label columns with unique part of histogram names + for i,hist in enumerate(histLabels(G2frame)[1]): + if i == 1: + HAPSizer.Add(wx.StaticText(HAPScroll,label='Copy'), + 0,wx.ALIGN_CENTER) + HAPSizer.Add(wx.StaticText(HAPScroll,label=f"\u25A1 = {hist}"), + 0,wx.ALIGN_CENTER) + w0 = wx.StaticText(lblScroll,label=' ') + lblSizer.Add(w0) + lblSizer.Add(wx.StaticText(lblScroll,label=' Ref ')) + firstentry,lblDict = displayDataArray(rowLabels,prmArray,HAPSizer,HAPScroll, + lblRow=True,lblSizer=lblSizer,lblPanel=lblScroll) + # get row sizes in data table + HAPSizer.Layout() + rowHeights = HAPSizer.GetRowHeights() + # set row sizes in Labels + # (must be done after HAPSizer row heights are defined) + s = wx.Size(-1,rowHeights[0]) + w0.SetMinSize(s) + for i,row in enumerate(rowLabels): + s = wx.Size(-1,rowHeights[i+1]) + lblDict[row].SetMinSize(s) + # Fit the scrolled windows to their content + lblSizer.Layout() + xLbl,_ = lblSizer.GetMinSize() + xTab,yTab = HAPSizer.GetMinSize() + lblScroll.SetSize((xLbl,yTab)) + lblScroll.SetMinSize((xLbl+15,yTab)) # add room for scroll bar + lblScroll.SetVirtualSize(lblSizer.GetMinSize()) + HAPScroll.SetVirtualSize(HAPSizer.GetMinSize()) + lblScroll.SetupScrolling(scroll_x=True, scroll_y=True, rate_x=20, rate_y=20) + HAPScroll.SetupScrolling(scroll_x=True, scroll_y=True, rate_x=20, rate_y=20) + if firstentry is not None: # prevent scroll to show last entry + wx.Window.SetFocus(firstentry) + firstentry.SetInsertionPoint(0) # prevent selection of text in widget + + #G2frame.dataWindow.ClearData() + + # layout the HAP window. This has histogram and phase info, so a + # notebook is needed for phase name selection. (That could + # be omitted for single-phase refinements, but better to remind the + # user of the phase + # topSizer = G2frame.dataWindow.topBox + # topParent = G2frame.dataWindow.topPanel + midPanel = G2frame.dataWindow + mainSizer = wx.BoxSizer(wx.VERTICAL) + #botSizer = G2frame.dataWindow.bottomBox + #botParent = G2frame.dataWindow.bottomPanel + + G2G.HorizontalLine(mainSizer,midPanel) + midPanel.SetSizer(mainSizer) + if not Phases: + mainSizer.Add(wx.StaticText(midPanel, + label='There are no phases in use')) + G2frame.dataWindow.SetDataSize() + return + # notebook for phases + HAPBook = G2G.GSNoteBook(parent=midPanel) + mainSizer.Add(HAPBook,1,wx.ALL|wx.EXPAND,1) + HAPtabs = [] + phaseList = [] + for phaseName in Phases: + phaseList.append(phaseName) + HAPtabs.append(wx.Panel(HAPBook)) + HAPBook.AddPage(HAPtabs[-1],phaseName) + HAPBook.Bind(wx.aui.EVT_AUINOTEBOOK_PAGE_CHANGED, selectPhase) + + page = 0 + HAPBook.SetSelection(page) + selectPhase(None) + #G2frame.dataWindow.SetDataSize() + +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] + 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]: + indexDict[hist] = {} + # phase fraction + indexDict[hist]['Phase frac'] = { + 'val' : ('Scale',0), + 'ref' : ('Scale',1),} + 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),} + elif PhaseData['Histograms'][hist]['Size'][0] == 'uniaxial': + indexDict[hist]['Size/Eq'] = { + 'val' : ('Size',1,0), + 'ref' : ('Size',2,0),} + indexDict[hist]['Size/Ax'] = { + 'val' : ('Size',1,1), + 'ref' : ('Size',2,1),} + 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),} + indexDict[hist]['Size LGmix'] = { + 'val' : ('Size',1,2), + 'ref' : ('Size',2,2),} + # microstrain values + if PhaseData['Histograms'][hist]['Mustrain'][0] == 'isotropic': + indexDict[hist]['\u00B5Strain'] = { + 'val' : ('Mustrain',1,0), + 'ref' : ('Mustrain',2,0),} + elif PhaseData['Histograms'][hist]['Mustrain'][0] == 'uniaxial': + indexDict[hist]['\u00B5Strain/Eq'] = { + 'val' : ('Mustrain',1,0), + 'ref' : ('Mustrain',2,0),} + indexDict[hist]['\u00B5Strain/Ax'] = { + 'val' : ('Mustrain',1,1), + 'ref' : ('Mustrain',2,1),} + 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),} + 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),} + + # Hydrostatic terms + Hsnames = G2spc.HStrainNames(SGData) + for i,lbl in enumerate(Hsnames): + if i >= len(PhaseData['Histograms'][hist]['HStrain'][0]): break + indexDict[hist][f'Size/{lbl}'] = { + 'val' : ('HStrain',0,i), + 'ref' : ('HStrain',1,i),} + + # Preferred orientation terms + if PhaseData['Histograms'][hist]['Pref.Ori.'][0] == 'MD': + indexDict[hist]['March-Dollase'] = { + 'val' : ('Pref.Ori.',1), + 'ref' : ('Pref.Ori.',2),} + 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), + } + 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),} + if 'Extinction' in PhaseData['Histograms'][hist]: + indexDict[hist]['Extinction'] = { + 'val' : ('Extinction',0), + 'ref' : ('Extinction',1),} + if 'Babinet' in PhaseData['Histograms'][hist]: + indexDict[hist]['Babinet A'] = { + 'val' : ('Babinet','BabA',0), + 'ref' : ('Babinet','BabA',1),} + if 'Babinet' in PhaseData['Histograms'][hist]: + indexDict[hist]['Babinet U'] = { + 'val' : ('Babinet','BabU',0), + 'ref' : ('Babinet','BabU',1),} + return indexDict diff --git a/GSASII/GSASIImiscGUI.py b/GSASII/GSASIImiscGUI.py index daac1cf8..7e12fa40 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 @@ -554,6 +552,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 @@ -575,6 +575,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') @@ -582,6 +590,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': @@ -720,7 +730,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/GSASIIplot.py b/GSASII/GSASIIplot.py index d6332d88..8544174c 100644 --- a/GSASII/GSASIIplot.py +++ b/GSASII/GSASIIplot.py @@ -214,14 +214,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} @@ -325,6 +325,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 @@ -395,7 +396,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 @@ -409,9 +410,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 @@ -420,8 +420,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 @@ -429,6 +430,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 @@ -444,7 +446,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': @@ -469,8 +471,60 @@ def FindPlotTab(self,label,Type,newImage=True,publish=None): Page.helpKey = self.G2frame.dataWindow.helpKey except AttributeError: Page.helpKey = 'HelpIntro' + Page.toolbar.enableArrows() # Disable Arrow keys if present 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. @@ -493,9 +547,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 @@ -606,7 +660,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 @@ -631,16 +685,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) @@ -664,6 +727,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()): @@ -679,6 +763,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] @@ -736,6 +836,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 7b5f449e..52609dc5 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: @@ -120,19 +129,6 @@ def ReplotPattern(G2frame,newPlot,plotType,PatternName=None,PickName=None): G2frame.Extinct = [] # array of extinct reflections PlotPatterns(G2frame,plotType=plotType) -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 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 PlotPatterns(G2frame,newPlot=False,plotType='PWDR',data=None, extraKeys=[],refineMode=False,indexFrom='',fromTree=False): '''Powder pattern plotting package - displays single or multiple powder @@ -198,17 +194,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,17 +220,26 @@ 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 - elif event.key == 'x'and 'PWDR' in plottype: + 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 == '.': 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: @@ -240,9 +247,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 @@ -310,6 +318,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. @@ -335,8 +344,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: @@ -350,8 +359,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: @@ -367,17 +379,24 @@ def OnPlotKeyPress(event): y1, y2= Page.figure.axes[0].get_ylim() Page.vLine = Plot.axvline(Page.startExclReg,color='b',dashes=(2,3)) Page.canvas.draw() - else: + else: # second key press Page.savedplot = None - 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): @@ -401,10 +420,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']: @@ -417,9 +443,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': @@ -429,7 +462,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) @@ -472,6 +505,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 @@ -486,7 +524,7 @@ def OnMotion(event): global PlotList G2plt.SetCursor(Page) # excluded region animation - if Page.excludeMode and Page.savedplot: + if Page.excludeMode and Page.savedplot: # 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 Page.savedplot = None @@ -494,7 +532,7 @@ def OnMotion(event): wx.CallAfter(PlotPatterns,G2frame,newPlot=False, plotType=plottype,extraKeys=extraKeys) return - else: + else: # mouse is out of plot region, give up on this region Page.canvas.restore_region(Page.savedplot) Page.vLine.set_xdata([event.xdata,event.xdata]) if G2frame.Weight: @@ -502,7 +540,7 @@ def OnMotion(event): else: axis = Page.figure.gca() axis.draw_artist(Page.vLine) - Page.canvas.blit(axis.bbox) + Page.canvas.blit(Page.figure.bbox) return elif Page.excludeMode or Page.savedplot: # reset if out of mode somehow Page.savedplot = None @@ -642,7 +680,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): #ugh - this removes a matplotlib error for mouse clicks in log plots np.seterr(invalid='ignore') @@ -702,9 +740,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 Page.canvas.restore_region(savedplot) G2frame.itemPicked.set_data([event.xdata], [event.ydata]) @@ -713,28 +757,40 @@ def OnDragMarker(event): else: axis = Page.figure.gca() axis.draw_artist(G2frame.itemPicked) - Page.canvas.blit(axis.bbox) + Page.canvas.blit(Page.figure.bbox) 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 Page.canvas.restore_region(savedplot) 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: axis = Page.figure.gca() axis.draw_artist(G2frame.itemPicked) - Page.canvas.blit(axis.bbox) + Page.canvas.blit(Page.figure.bbox) 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()) @@ -753,14 +809,20 @@ def OnDragLabel(event): else: axis = Page.figure.gca() axis.draw_artist(G2frame.itemPicked) - Page.canvas.blit(axis.bbox) + Page.canvas.blit(Page.figure.bbox) except: pass 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 Page.canvas.restore_region(savedplot) if Page.pickTicknum: @@ -779,12 +841,19 @@ def OnDragTickmarks(event): coords[1][:] = pos Page.tickDict[phase].set_data(coords) axis.draw_artist(Page.tickDict[phase]) - Page.canvas.blit(axis.bbox) + Page.canvas.blit(Page.figure.bbox) 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 Page.canvas.restore_region(savedplot) coords = G2frame.itemPicked.get_data() @@ -792,8 +861,8 @@ def OnDragDiffCurve(event): Page.diffOffset = -event.ydata G2frame.itemPicked.set_data(coords) Page.figure.gca().draw_artist(G2frame.itemPicked) # Diff curve only found in 1-window plot - Page.canvas.blit(Page.figure.gca().bbox) - + Page.canvas.blit(Page.figure.bbox) + def DeleteHKLlabel(HKLmarkers,key): '''Delete an HKL label''' del HKLmarkers[key[0]][key[1]] @@ -820,6 +889,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 @@ -938,20 +1012,24 @@ 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 if G2frame.ifSetLimitsMode == 3: # add an excluded region excl = [0,0] excl[0] = max(limData[1][0],min(xy[0],limData[1][1])) excl[1] = excl[0]+0.1 limData.append(excl) - elif G2frame.ifSetLimitsMode == 2: # set upper - limData[1][1] = max(xy[0],limData[1][0]) + elif 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) @@ -1087,6 +1165,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] @@ -1433,12 +1512,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: @@ -1600,7 +1686,90 @@ def onPartialConfig(event): Page.plotStyle['partials'] = True Replot() configPartialDisplay(G2frame,Page.phaseColors,Replot) - + 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 @@ -1619,6 +1788,19 @@ def onPartialConfig(event): 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 @@ -1630,7 +1812,24 @@ def onPartialConfig(event): 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 + if G2frame.ifSetLimitsMode and G2frame.GPXtree.GetItemText(G2frame.GPXtree.GetSelection()) == 'Limits': # note mode if G2frame.ifSetLimitsMode == 1: @@ -1644,7 +1843,7 @@ def onPartialConfig(event): Page.excludeMode = False # True when defining an excluded region Page.savedplot = None #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),}) @@ -1656,7 +1855,8 @@ def onPartialConfig(event): 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')) @@ -1696,7 +1896,8 @@ def onPartialConfig(event): 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 @@ -1714,7 +1915,7 @@ def onPartialConfig(event): 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') @@ -1732,17 +1933,21 @@ def onPartialConfig(event): 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 - for b in Page.bindings: + # redo plot binding each time the Plot is updated since values + # may have been changed after 1st call + try: + G2frame.PlotBindings + except: + G2frame.PlotBindings = [] + for b in G2frame.PlotBindings: Page.canvas.mpl_disconnect(b) - Page.bindings = [] - Page.bindings.append(Page.canvas.mpl_connect('key_press_event', OnPlotKeyPress)) + 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 @@ -1781,7 +1986,8 @@ def onPartialConfig(event): 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 = [] @@ -1808,8 +2014,9 @@ def onPartialConfig(event): 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 @@ -1822,16 +2029,15 @@ def onPartialConfig(event): ifLimits = False if G2frame.GPXtree.GetItemText(G2frame.PickId) == 'Limits': ifLimits = True - Page.plotStyle['qPlot'] = False - Page.plotStyle['dPlot'] = False # keys in use for graphics control: - # 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', @@ -1839,11 +2045,13 @@ def onPartialConfig(event): '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']: @@ -1857,6 +2065,7 @@ def onPartialConfig(event): 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: @@ -1885,6 +2094,7 @@ def onPartialConfig(event): 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: @@ -1895,7 +2105,7 @@ def onPartialConfig(event): 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) @@ -1943,7 +2153,7 @@ def onPartialConfig(event): 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 = [] @@ -1999,8 +2209,7 @@ def onPartialConfig(event): 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']: @@ -2010,9 +2219,9 @@ def onPartialConfig(event): 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.' @@ -2023,8 +2232,266 @@ def onPartialConfig(event): 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: @@ -2033,10 +2500,12 @@ def onPartialConfig(event): # 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): @@ -2086,9 +2555,9 @@ def onPartialConfig(event): 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]) @@ -2205,7 +2674,6 @@ def onPartialConfig(event): 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 @@ -2236,9 +2704,9 @@ def onPartialConfig(event): 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.)) @@ -2443,12 +2911,12 @@ def onPartialConfig(event): 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) @@ -2558,7 +3026,7 @@ def onPartialConfig(event): # 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) @@ -2608,7 +3076,7 @@ def onPartialConfig(event): 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: @@ -2680,42 +3148,7 @@ def onPartialConfig(event): 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 @@ -2897,7 +3330,10 @@ def onPartialConfig(event): 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 @@ -4414,3 +4850,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/meson.build b/GSASII/meson.build index b9514904..e6c5cb7f 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',