Skip to content

Commit 3b253ee

Browse files
committed
Merge remote-tracking branch 'remotes/origin/main'
2 parents cf06e58 + 35c01bd commit 3b253ee

File tree

7 files changed

+297
-13
lines changed

7 files changed

+297
-13
lines changed

.github/copilot-instructions.md

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
# GSAS-II - Crystallography Software Package
2+
3+
GSAS-II is a comprehensive Python package for analysis of x-ray and neutron diffraction data, including single-crystal, powder, and time-of-flight data. It includes both GUI (wxPython) and scriptable interfaces, with compiled Fortran extensions for performance-critical computations.
4+
5+
Always reference these instructions first and fallback to search or bash commands only when you encounter unexpected information that does not match the info here.
6+
7+
## Working Effectively
8+
9+
### Bootstrap and Build the Repository
10+
```bash
11+
# Install core build dependencies
12+
python -m pip install --upgrade pip setuptools wheel
13+
python -m pip install meson-python ninja numpy cython scipy pycifrw
14+
15+
# Build the package (takes ~16 seconds, NEVER CANCEL, set timeout to 30+ minutes)
16+
python -m build -wnx --no-isolation
17+
18+
# OR install in editable mode for development (takes ~12 seconds, NEVER CANCEL, set timeout to 30+ minutes)
19+
python -m pip install -e . --no-build-isolation
20+
```
21+
22+
### Install Optional Dependencies
23+
```bash
24+
# For GUI functionality
25+
python -m pip install wxpython matplotlib pyopengl
26+
27+
# For additional features
28+
python -m pip install pillow h5py imageio requests gitpython pybaselines
29+
30+
# For testing and development
31+
python -m pip install pytest nox
32+
```
33+
34+
### Run Tests
35+
```bash
36+
# Add compiled executables to PATH before running tests
37+
export PATH="$PATH:$(pwd)/build/cp312/sources"
38+
39+
# Alternative: Copy executables to a location in PATH (one-time setup)
40+
# mkdir -p ~/.local/bin
41+
# cp build/cp312/sources/{LATTIC,convcell} ~/.local/bin/
42+
43+
# Run all tests (some require network connectivity, ~3-4 seconds for working tests)
44+
python -m pytest tests/ -v
45+
46+
# Run specific test modules that work offline
47+
python -m pytest tests/test_lattice.py tests/test_nistlat.py tests/test_elm.py -v
48+
49+
# Using nox for testing workflow
50+
python -m nox -s tests
51+
```
52+
53+
### Validation Scenarios
54+
Always run these validation steps after making changes:
55+
56+
1. **Basic Import Test**:
57+
```bash
58+
python -c "import GSASII; print('GSAS-II import successful')"
59+
```
60+
61+
2. **Scriptable Interface Test**:
62+
```bash
63+
python -c "
64+
import GSASII.GSASIIscriptable as G2sc
65+
gpx = G2sc.G2Project(newgpx='/tmp/test.gpx')
66+
print('GSASIIscriptable working correctly')
67+
"
68+
```
69+
70+
3. **Binary Extensions Test**:
71+
```bash
72+
python -c "
73+
import GSASII.GSASIIlattice as G2lat
74+
import GSASII.GSASIIspc as G2spc
75+
print('Compiled extensions working')
76+
"
77+
```
78+
79+
4. **Run Core Functionality Tests**:
80+
```bash
81+
export PATH="$PATH:$(pwd)/build/cp312/sources"
82+
python -m pytest tests/test_lattice.py::test_Brav -v
83+
```
84+
85+
## Build System Details
86+
87+
### Timing Expectations
88+
- **NEVER CANCEL**: Build takes ~16 seconds on typical hardware but may take up to 30 minutes on slower systems
89+
- **NEVER CANCEL**: Editable install takes ~12 seconds but may take up to 30 minutes
90+
- **NEVER CANCEL**: Test suite takes 3-4 seconds for working tests but up to 10 minutes with all tests
91+
- Set explicit timeouts of 30+ minutes for build commands and 15+ minutes for test commands
92+
93+
### Build Architecture
94+
- Uses **meson** build system with **f2py** for Fortran compilation
95+
- Compiles Fortran extensions: pyspg, pydiffax, pypowder, pytexture, pack_f, histogram2d, unpack_cbf
96+
- Builds standalone executables: LATTIC, convcell
97+
- Requires gfortran compiler
98+
99+
### Build Troubleshooting
100+
- If binaries are missing: run `python -m pip install -e . --no-build-isolation`
101+
- For PATH issues with executables: `export PATH="$PATH:$(pwd)/build/cp312/sources"`
102+
- Build output location: `./build/cp312/sources/` (for Python 3.12)
103+
104+
## Running Applications
105+
106+
### Scriptable Interface (Recommended for Development)
107+
```bash
108+
python -c "
109+
import GSASII.GSASIIscriptable as G2sc
110+
gpx = G2sc.G2Project(newgpx='project.gpx')
111+
# Add your GSAS-II scripting code here
112+
"
113+
```
114+
115+
### GUI Application
116+
```bash
117+
# Note: GUI requires display/X11 - will not work in headless environments
118+
python -m GSASII
119+
```
120+
121+
## Development Workflow
122+
123+
### Linting and Code Quality
124+
```bash
125+
# Using nox (recommended)
126+
python -m nox -s lint # Pre-commit hooks and formatting
127+
python -m nox -s pylint # Static code analysis
128+
129+
# Manual linting
130+
python -m pip install pre-commit
131+
pre-commit run --all-files
132+
```
133+
134+
### Documentation
135+
```bash
136+
# Build documentation
137+
python -m nox -s docs
138+
139+
# Build and serve docs locally
140+
python -m nox -s docs -- --serve
141+
```
142+
143+
### Common Development Tasks
144+
Always validate changes by running:
145+
1. Build the package: `python -m build -wnx --no-isolation`
146+
2. Install in editable mode: `python -m pip install -e . --no-build-isolation`
147+
3. Add executables to PATH: `export PATH="$PATH:$(pwd)/build/cp312/sources"`
148+
4. Run working tests: `python -m pytest tests/test_lattice.py tests/test_elm.py -v`
149+
5. Test core functionality: Run the validation scenarios above
150+
151+
## Troubleshooting
152+
153+
### Common Issues
154+
- **"binary load error: pyspg not found"**: Run editable install to compile binaries
155+
- **"FileNotFoundError: 'LATTIC'"**: Add `./build/cp312/sources` to PATH
156+
- **Network connectivity test failures**: Normal in restricted environments, focus on offline tests
157+
- **wxPython GUI issues**: GUI requires display, use scriptable interface in headless environments
158+
159+
### Dependencies Not Found
160+
Install the specific missing dependency:
161+
```bash
162+
# For seekpath k-vector functionality
163+
python -m pip install seekpath
164+
165+
# For complete GUI experience
166+
python -m pip install wxpython matplotlib pyopengl pillow h5py
167+
```
168+
169+
## Key Projects in Codebase
170+
171+
### Main Modules
172+
- **GSASII/**: Main package directory
173+
- **GSASIIscriptable.py**: Primary API for programmatic use
174+
- **GSASIIGUI.py**: Main GUI application entry point
175+
- **GSASIIdata.py**: Core data structures and file I/O
176+
- **GSASIImath.py**: Mathematical routines for crystallography
177+
- **GSASIIlattice.py**: Lattice parameter and space group operations
178+
- **GSASIIspc.py**: Space group and symmetry operations
179+
180+
### Build Files
181+
- **meson.build**: Main build configuration
182+
- **sources/meson.build**: Fortran extension build configuration
183+
- **pyproject.toml**: Package metadata and dependencies
184+
185+
### Testing
186+
- **tests/**: Self-contained test suite
187+
- **test_lattice.py**: Lattice and crystallography tests (always work offline)
188+
- **test_nistlat.py**: NIST lattice utility tests (require executables in PATH)
189+
- **test_scriptref.py**: Scripting interface integration tests
190+
191+
### Documentation
192+
- **docs/**: Sphinx documentation source
193+
- **README.md**: Project overview and links
194+
195+
Always check binary compilation status and add executables to PATH before running tests or using advanced functionality.

GSASII/GSASIIdataGUI.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7056,7 +7056,7 @@ def _makemenu(): # routine to create menu when first used
70567056
# IMG / Masks
70577057
G2G.Define_wxId('wxID_MASKCOPY', 'wxID_MASKSAVE', 'wxID_MASKLOAD', 'wxID_NEWMASKSPOT', 'wxID_NEWMASKARC', 'wxID_NEWMASKRING',
70587058
'wxID_NEWMASKFRAME', 'wxID_NEWMASKPOLY','wxID_NEWMASKXLINE','wxID_NEWMASKYLINE','wxID_MASKLOADNOT',
7059-
'wxID_FINDSPOTS', 'wxID_AUTOFINDSPOTS', 'wxID_DELETESPOTS','wxID_MASKCOPYSELECTED')
7059+
'wxID_FINDSPOTS', 'wxID_AUTOFINDSPOTS', 'wxID_DELETESPOTS','wxID_MASKCOPYSELECTED', 'wxID_LOADSPOTS')
70607060
def _makemenu(): # routine to create menu when first used
70617061
self.MaskMenu = wx.MenuBar()
70627062
self.PrefillDataMenu(self.MaskMenu)
@@ -7069,7 +7069,8 @@ def _makemenu(): # routine to create menu when first used
70697069
self.MaskEdit.Append(G2G.wxID_MASKSAVE,'Save mask','Save mask to file')
70707070
self.MaskEdit.Append(G2G.wxID_MASKLOADNOT,'Load mask','Load mask from file; ignoring threshold')
70717071
self.MaskEdit.Append(G2G.wxID_MASKLOAD,'Load mask w/threshold','Load mask from file keeping the threshold value')
7072-
self.MaskEdit.Append(G2G.wxID_FINDSPOTS,'Pixel mask search','Search for pixels to mask; NB: slow')
7072+
self.MaskEdit.Append(G2G.wxID_FINDSPOTS,'Pixel mask search','Search for pixels to mask')
7073+
self.MaskEdit.Append(G2G.wxID_LOADSPOTS,'Load pixel mask','Use non-zero pixels in an image to mask')
70737074
self.MaskEdit.Append(G2G.wxID_AUTOFINDSPOTS,'Multi-IMG pixel mask search','Search multiple images for pixels to mask; NB: slow')
70747075
self.MaskEdit.Append(G2G.wxID_DELETESPOTS,'Delete spot masks','Delete all spot masks')
70757076
submenu.Append(G2G.wxID_NEWMASKARC,'Arc mask','Create an arc mask with mouse input')

GSASII/GSASIIimgGUI.py

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2000,6 +2000,55 @@ def OnLoadMask(event):
20002000
finally:
20012001
dlg.Destroy()
20022002

2003+
def OnLoadPixelMask(event):
2004+
Names = G2gd.GetGPXtreeDataNames(G2frame,['IMG ',])
2005+
img = G2frame.GPXtree.GetItemText(G2frame.Image)
2006+
if img not in Names: # remove current entry from list of names
2007+
print(f'Strange: {img} not in IMG entries')
2008+
else:
2009+
del Names[Names.index(img)]
2010+
# if nothing left, provide error & quit
2011+
if not Names:
2012+
G2G.G2MessageBox(G2frame,
2013+
'Before you can use this command you must import the mask you want to use as an image',
2014+
'No mask image')
2015+
return
2016+
elif len(Names) == 1:
2017+
sel = Names[0]
2018+
else:
2019+
dlg = G2G.G2SingleChoiceDialog(G2frame,
2020+
'Select an image as a pixel mask:',
2021+
'Load Pixel Mask',Names)
2022+
dlg.CenterOnParent()
2023+
if dlg.ShowModal() == wx.ID_OK:
2024+
sel = dlg.GetSelection()
2025+
dlg.Destroy()
2026+
else:
2027+
dlg.Destroy()
2028+
return
2029+
# Got our mask identified
2030+
maskID = G2gd.GetGPXtreeItemId(G2frame,G2frame.root,sel)
2031+
maskdata = G2frame.GPXtree.GetItemPyData(
2032+
G2gd.GetGPXtreeItemId(G2frame,maskID,'Image Controls'))
2033+
formatName = maskdata.get('formatName','')
2034+
Npix,maskfile,imagetag = G2IO.GetCheckImageFile(G2frame,maskID)
2035+
# check it matches
2036+
imgdim = G2frame.GPXtree.GetItemPyData(G2frame.Image)[0]
2037+
if Npix != imgdim:
2038+
G2G.G2MessageBox(G2frame,
2039+
f'Mask dimensions ({Npix}) != Image ({imgdim}). Images do not match',
2040+
'Mask image wrong dimensions')
2041+
return
2042+
maskImage = np.array(G2fil.GetImageData(G2frame,maskfile,True,ImageTag=imagetag,FormatName=formatName),dtype=np.float32)
2043+
data['SpotMask']['MaskLoaded'] = sel # provides a record that this
2044+
# was loaded rather than a search
2045+
data['SpotMask']['spotMask'] = maskImage > 0
2046+
nmasked = sum(data['SpotMask']['spotMask'].flatten())
2047+
frac = nmasked/data['SpotMask']['spotMask'].size
2048+
G2G.G2MessageBox(G2frame,
2049+
f'Mask removes {nmasked} pixels ({frac:.3f}%)',
2050+
'Mask loaded')
2051+
20032052
def OnFindPixelMask(event):
20042053
'''Do auto search for pixels to mask
20052054
Called from (Masks) Operations->"Pixel mask search"
@@ -2343,6 +2392,7 @@ def OnAzimuthPlot(event):
23432392
G2frame.Bind(wx.EVT_MENU, OnLoadMask, id=G2G.wxID_MASKLOADNOT)
23442393
G2frame.Bind(wx.EVT_MENU, OnSaveMask, id=G2G.wxID_MASKSAVE)
23452394
G2frame.Bind(wx.EVT_MENU, OnFindPixelMask, id=G2G.wxID_FINDSPOTS)
2395+
G2frame.Bind(wx.EVT_MENU, OnLoadPixelMask, id=G2G.wxID_LOADSPOTS)
23462396
G2frame.Bind(wx.EVT_MENU, OnAutoFindPixelMask, id=G2G.wxID_AUTOFINDSPOTS)
23472397
G2frame.Bind(wx.EVT_MENU, OnDeleteSpotMask, id=G2G.wxID_DELETESPOTS)
23482398
G2frame.Bind(wx.EVT_MENU, ToggleSpotMaskMode, id=G2G.wxID_NEWMASKSPOT)

GSASII/GSASIIpwdplot.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1386,7 +1386,9 @@ def onPlotFormat(event):
13861386

13871387
def refPlotUpdate(Histograms,cycle=None,restore=False):
13881388
'''called to update an existing plot during a Rietveld fit; it only
1389-
updates the curves, not the reflection marks or the legend
1389+
updates the curves, not the reflection marks or the legend.
1390+
It should be called with restore=True to reset plotting
1391+
parameters after the refinement is done.
13901392
'''
13911393
if restore:
13921394
(G2frame.SinglePlot,G2frame.Contour,G2frame.Weight,
@@ -1396,7 +1398,7 @@ def refPlotUpdate(Histograms,cycle=None,restore=False):
13961398
if plottingItem not in Histograms:
13971399
histoList = [i for i in Histograms.keys() if i.startswith('PWDR ')]
13981400
if len(histoList) == 0:
1399-
print('Skipping plot, no PWDR item found!')
1401+
print('Skipping plot update, no PWDR items!')
14001402
return
14011403
plotItem = histoList[0]
14021404
else:
@@ -1418,6 +1420,9 @@ def refPlotUpdate(Histograms,cycle=None,restore=False):
14181420
Ifin = np.searchsorted(X,limits[1][1])
14191421
if Ibeg == Ifin: # if no points are within limits bad things happen
14201422
Ibeg,Ifin = 0,None
1423+
elif Ibeg > Ifin:
1424+
Ifin, Ibeg = Ibeg, Ifin # TOF with order reversed
1425+
14211426
if Page.plotStyle['sqrtPlot']:
14221427
olderr = np.seterr(invalid='ignore') #get around sqrt(-ve) error
14231428
Y = np.where(xye[1]>=0.,np.sqrt(xye[1]),-np.sqrt(-xye[1]))
@@ -1454,6 +1459,7 @@ def refPlotUpdate(Histograms,cycle=None,restore=False):
14541459
else:
14551460
Plot.set_title(Title)
14561461
Page.canvas.draw()
1462+
wx.GetApp().Yield()
14571463

14581464
def incCptn(string):
14591465
'''Adds a underscore to "hide" a MPL object from the legend if

0 commit comments

Comments
 (0)