SusMat-SWAXS 2.1.3__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
smswaxs/__init__.py ADDED
@@ -0,0 +1,32 @@
1
+ """
2
+ SusMat-SWAXS: A collection of python scripts for SWAXS data processing and conversion.
3
+
4
+ This package provides tools for:
5
+ - SAXS/WAXS data integration from Xenocs Xeuss instruments
6
+ - Data format conversions (EDF to HDF5, TIFF, etc.)
7
+ - Background subtraction and absorption corrections
8
+ - Data visualization and analysis
9
+ """
10
+
11
+ __version__ = "2.1.3"
12
+ __author__ = "Samuel Eyley"
13
+ __email__ = "samuel.eyley@kuleuven.be"
14
+
15
+ # Import CLI functions for programmatic access
16
+ from .dat_conv import cli as dat_conv_cli
17
+ from .xeuss_edf_h5_multi import add_edfs_to_hdf5
18
+ from .saxs_integration_xeuss import cli as saxs_integration_cli
19
+ from .waxs_integration_xeuss import cli as waxs_integration_cli
20
+ from .sph_harm import cli as sph_harm_cli
21
+
22
+ # Make CLI functions available at package level
23
+ __all__ = [
24
+ "__version__",
25
+ "__author__",
26
+ "__email__",
27
+ "dat_conv_cli",
28
+ "add_edfs_to_hdf5",
29
+ "saxs_integration_cli",
30
+ "waxs_integration_cli",
31
+ "sph_harm_cli"
32
+ ]
smswaxs/dat_conv.py ADDED
@@ -0,0 +1,136 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Thu May 7 11:44:10 2015
4
+
5
+ @author: u0092172
6
+ """
7
+ import argparse as ap
8
+ import re
9
+ import numpy as np
10
+
11
+ def get_line_number(phrase, file_name):
12
+ """Get the line number containing phrase"""
13
+ with open(file_name, encoding="utf-8") as f:
14
+ for i, line in enumerate(f, 1):
15
+ p = re.compile(phrase)
16
+ if p.match(line) is not None:
17
+ return i
18
+
19
+ def get_number_lines(file_name):
20
+ """Get the number of lines in a file"""
21
+ with open(file_name, encoding="utf-8") as f:
22
+ lines = len(f.readlines())
23
+ return lines
24
+
25
+
26
+ def cli():
27
+ """Convert dat files to xye format for use in TOPAS"""
28
+ parser = ap.ArgumentParser(description='Convert dat files to xye format for use in TOPAS')
29
+ parser.add_argument("input", help="Filename of the .dat (excl. ext.)")
30
+ parser.add_argument(
31
+ "--wavelength", help="Wavelength in Angstroms", default=float(1.54189))
32
+ parser.add_argument(
33
+ "--highscore", help="Output for highscore plus", action="store_true")
34
+ parser.add_argument(
35
+ "--exposure", help="Counting time (convert to counts from CPS)", type=float, default=1)
36
+ # parser.add_argument("--dat", help="convert from dat file", action="store_true")
37
+ parser.add_argument(
38
+ "--SAXS", help="output SAXS data (q and don't cut low angle)", action="store_true")
39
+ parser.add_argument(
40
+ "--renormalize", help="boost counts", action="store_true")
41
+ parser.add_argument("--clip", help="clip range", action="store_true")
42
+ parser.add_argument(
43
+ "--fullprof", help="general fullprof input", action="store_true")
44
+ parser.add_argument(
45
+ "--kratky", help="Generate Kratky plot columns", action="store_true")
46
+ parser.add_argument("--pyfai", help="pyfai input", action="store_true")
47
+ parser.add_argument(
48
+ "--zero", help="set minimum to zero", action="store_true")
49
+ args = parser.parse_args() # map arguments to args
50
+
51
+ fname = args.input
52
+ if args.highscore is True:
53
+ ext = ".asc"
54
+ elif args.fullprof is True:
55
+ ext = "_FP.dat"
56
+ elif args.kratky is True:
57
+ ext = "_kratky.dat"
58
+ else:
59
+ ext = ".xye"
60
+
61
+ # Import the data
62
+
63
+ if args.kratky is True:
64
+ args.SAXS = True
65
+
66
+ if args.pyfai is True:
67
+ data = np.genfromtxt(fname, comments='#',
68
+ usecols=(0, 1, 2), encoding="utf-8")
69
+ else:
70
+ head = get_line_number('.*q\\([AÅ].*', fname)
71
+ data = np.genfromtxt(fname, skip_header=head,
72
+ usecols=(0, 1, 2), encoding="utf-8")
73
+
74
+ data = data[~np.isnan(data).any(axis=1)]
75
+
76
+ q = data[:, 0]
77
+ i = data[:, 1]
78
+ e = data[:, 2]
79
+
80
+ if args.pyfai is True:
81
+ ttheta = q
82
+ if args.clip is True:
83
+ start = np.where(ttheta > 5)[0][0]
84
+ ttheta = ttheta[start:]
85
+ i = i[start:]
86
+ e = e[start:]
87
+ elif args.SAXS is False:
88
+ # Calc 2theta
89
+
90
+ ttheta = 2*np.degrees(np.arcsin((q*args.wavelength)/(4*np.pi)))
91
+
92
+ # Clip the data to start at 5 degrees to remove excessive intensity
93
+ if args.clip is True:
94
+ start = np.where(ttheta > 5)[0][0]
95
+ ttheta = ttheta[start:]
96
+ i = i[start:]
97
+ e = e[start:]
98
+
99
+ # Convert i to counts
100
+ if args.renormalize is True:
101
+ i = i*args.exposure*1e11
102
+ else:
103
+ i = i*args.exposure
104
+
105
+ if min(i) < 0:
106
+ i -= min(i)
107
+
108
+ if args.zero is True:
109
+ i -= min(i)
110
+
111
+ if args.highscore is True:
112
+ out = np.column_stack((ttheta.flatten(), i.flatten()))
113
+ else:
114
+ if args.SAXS is False:
115
+ out = np.column_stack((ttheta.flatten(), i.flatten(), e.flatten()))
116
+ elif args.kratky is False:
117
+ out = np.column_stack((q.flatten(), i.flatten(), e.flatten()))
118
+ else:
119
+ q2i = (q**2)*i
120
+ q2sig = (q**2)*e
121
+ out = np.column_stack(
122
+ (q.flatten(), i.flatten(), q2i.flatten(), e.flatten(), q2sig.flatten()))
123
+
124
+ if args.fullprof is True:
125
+ head2 = 'XYDATA' + '\n' + 'INTER 0 0 2' + 4*'\n'
126
+ elif args.kratky is True:
127
+ head2 = "q I(q) q2I(q) sig(q) q2sig(q)"
128
+ else:
129
+ head2 = ''
130
+
131
+ np.savetxt(fname[:-4]+ext, out, delimiter=' ', fmt='%.10f',
132
+ header=head2, comments='', newline='\n')
133
+
134
+
135
+ if __name__ == '__main__':
136
+ cli()
@@ -0,0 +1,142 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Spyder Editor
4
+
5
+ This is a temporary script file.
6
+ """
7
+
8
+ import h5py
9
+ import hdf5plugin
10
+ import pyFAI
11
+ import fabio
12
+ import numpy as np
13
+ import os
14
+ import glob
15
+ import matplotlib.pyplot as plt
16
+ from matplotlib.colors import LogNorm
17
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
18
+ import matplotlib.ticker as ticker
19
+ import argparse as ap
20
+
21
+ parser = ap.ArgumentParser(description=
22
+ "Perform data reduction on hdf5 data from DESY")
23
+
24
+ parser.add_argument("sample", help="sample file (.nxs)")
25
+
26
+ parser.add_argument("mask", help="mask file (.edf)")
27
+ parser.add_argument("poni", help="PONI configuration file (.poni)")
28
+ parser.add_argument("--empty", help="empty file (.nxs)")
29
+ parser.add_argument("--background", help="background file (.nxs)")
30
+
31
+ parser.add_argument("--bkg_factor", help="background correction factor",type=float,default=float(1))
32
+ parser.add_argument("--noscaling", help="background file (.nxs)",action="store_true")
33
+ parser.add_argument("--autobkg", help="Automatic background scaling",action="store_true")
34
+ args = parser.parse_args()
35
+
36
+ #path = "/home/u0092172/Documents/"
37
+ #folder = "DESY/"
38
+ #sample = "GA5/GA5_2.nxs"
39
+ #background = "water/water_2.nxs"
40
+ mask = fabio.open(args.mask)
41
+ poni = pyFAI.load(args.poni)#"config_saxs_sdetx_1500_12keV.poni")
42
+ file = h5py.File(args.sample,'r')
43
+
44
+ if args.noscaling == True:
45
+ transmission_sample = 1
46
+ else:
47
+ transmission_sample = file['scan']['data']['beamstop_2'][()]/file['scan']['data']['ic1'][()]
48
+ print("Sample transmission:"+str(round(transmission_sample,4)))
49
+
50
+ temperature = str(round(file['scan']['data']['p62']['t95tempproglinkam']['eh.01']['temperature'][()],2))
51
+ print("Sample temperature:"+temperature)
52
+
53
+
54
+ data_1D = poni.integrate1d(file['scan']['data']['saxs_raw'][()][0]/transmission_sample,
55
+ 1000, filename=args.sample+"_"+temperature+'_1D.dat', correctSolidAngle=True,
56
+ method='csr_ocl', radial_range=(None), azimuth_range=(None),
57
+ unit="q_A^-1", mask=mask.data, normalization_factor=1.0,
58
+ metadata=None,error_model="poisson")
59
+
60
+ if args.background != None:
61
+ bkg = h5py.File(args.background,'r')
62
+ if args.noscaling == True:
63
+ transmission_bkg = 1
64
+ else:
65
+ transmission_bkg = bkg['scan']['data']['beamstop_2'][()]/bkg['scan']['data']['ic1'][()]
66
+ try:
67
+ temperature_bkg = str(round(bkg['scan']['data']['p62']['t95tempproglinkam']['eh.01']['temperature'][()],2))
68
+ except KeyError:
69
+ temperature_bkg = str(-1000)
70
+ print("Background transmission:"+str(round(transmission_bkg,4)))
71
+ print("Background temperature:"+temperature_bkg)
72
+
73
+ bkg_1D = poni.integrate1d(bkg['scan']['data']['saxs_raw'][()][0]/transmission_bkg,
74
+ 1000, filename=args.sample+"_"+temperature+'_bkg1D.dat', correctSolidAngle=True,
75
+ method='csr_ocl', radial_range=(None), azimuth_range=(None),
76
+ unit="q_A^-1", mask=mask.data, normalization_factor=1.0,
77
+ metadata=None,error_model="poisson")
78
+ if args.autobkg == True:
79
+ print("Use automated background scaling with caution!")
80
+ if max(bkg_1D[1][:np.where(bkg_1D[0]>0.02)[0][0]]) > max(data_1D[1][:np.where(data_1D[0]>0.02)[0][0]]):
81
+ bkg_1D = [bkg_1D[0],bkg_1D[1],bkg_1D[2]]
82
+ bkg_1D[1] -= max(bkg_1D[1][:np.where(bkg_1D[0]>0.02)[0][0]]) - max(data_1D[1][:np.where(data_1D[0]>0.02)[0][0]])
83
+ else:
84
+ data_I = data_1D[1]
85
+ data_sig = abs(data_1D[2])
86
+ if args.empty != None:
87
+ empty = h5py.File(args.empty,'r')
88
+ if args.noscaling == True:
89
+ transmission_empty = 1
90
+ else:
91
+ transmission_empty = empty['scan']['data']['beamstop_2'][()]/empty['scan']['data']['ic1'][()]
92
+ empty_1D = poni.integrate1d(empty['scan']['data']['saxs_raw'][()][0]/transmission_empty,
93
+ 1000, filename=args.sample+"_"+temperature+'_empty1D.dat', correctSolidAngle=True,
94
+ method='csr_ocl', radial_range=(None), azimuth_range=(None),
95
+ unit="q_A^-1", mask=mask.data, normalization_factor=1.0,
96
+ metadata=None,error_model="poisson")
97
+ if args.autobkg == True:
98
+ if max(empty_1D[1][:np.where(empty_1D[0]>0.02)[0][0]]) > max(data_1D[1][:np.where(data_1D[0]>0.02)[0][0]]):
99
+ empty_1D[1] -= max(empty_1D[1][:np.where(empty_1D[0]>0.02)[0][0]]) - max(data_1D[1][:np.where(data_1D[0]>0.02)[0][0]])
100
+ if args.background != None:
101
+ data_I = (data_1D[1]-empty_1D[1])-(bkg_1D[1]-empty_1D[1])
102
+ data_sig = abs(data_1D[2])+abs(empty_1D[2])+abs(bkg_1D[2])+abs(empty_1D[2])
103
+ else:
104
+ data_I = data_1D[1]-empty_1D[1]
105
+ data_sig = abs(data_1D[2])+abs(empty_1D[2])
106
+ else:
107
+ if args.background == None:
108
+ data_I = data_1D[1]
109
+ data_sig = abs(data_1D[2])
110
+ else:
111
+ data_I = data_1D[1]-bkg_1D[1]
112
+ data_sig = abs(data_1D[2])+abs(bkg_1D[2])
113
+ fig, ax = plt.subplots(figsize=(25,10),constrained_layout=True)
114
+ """fig1 = ax[0].imshow(data_cor,norm=LogNorm(vmin=1e-1, vmax=10**3),cmap=plt.get_cmap('plasma'))
115
+ # fig1 = ax[0].imshow(data_cor,norm=LogNorm(vmin=300, vmax=10**5),cmap=plt.get_cmap('plasma'))
116
+ # create an axes on the right side of ax. The width of cax will be 5%
117
+ # of ax and the padding between cax and ax will be fixed at 0.05 inch.
118
+ divider = make_axes_locatable(ax[0])
119
+ cax = divider.append_axes("right", size="5%", pad=0.05)
120
+
121
+ cbar = plt.colorbar(fig1, cax=cax)
122
+ cbar.ax.tick_params(labelsize=25)
123
+
124
+ ax[0].set_xlabel('pixel',fontsize=25)
125
+ ax[0].set_ylabel('pixel',fontsize=25)
126
+ ax[0].tick_params(labelsize=25)
127
+ """
128
+ # fig2 = ax.plot(q, Intensity)
129
+ #fig2 = ax.plot(data_1D[0],data_I)
130
+ fig = ax.errorbar(data_1D[0],data_I,yerr=data_sig)
131
+ ax.set_xscale('log')
132
+ ax.set_yscale('log')
133
+ ax.set_xlabel(r'$Q\/[\AA^{-1}]$',fontsize=25)
134
+ ax.set_ylabel('intensity [a.u.]',fontsize=25)
135
+ # ax.set_xticks([0.01,0.1,0.4,0.6])
136
+ ax.get_xaxis().set_major_formatter(ticker.ScalarFormatter())
137
+ ax.tick_params(labelsize=25)
138
+ #plt.show()
139
+ plt.savefig(args.sample+"_"+temperature+".png", bbox_inches="tight", dpi=600)
140
+
141
+ data_out = np.column_stack((data_1D[0],data_I,data_sig))
142
+ np.savetxt(args.sample+"_subtracted.dat",data_out,delimiter='\t',fmt='%s')
@@ -0,0 +1,225 @@
1
+ """
2
+ This script will integrate 2D SAXS data collected on the Xenocs
3
+ Xeuss SWAXS instrument and subtract background data if necessary.
4
+ """
5
+ import argparse as ap
6
+ import matplotlib.ticker as ticker
7
+ import matplotlib.pyplot as plt
8
+ from matplotlib.colors import LogNorm
9
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
10
+ import pyFAI
11
+ from pyFAI.integrator.azimuthal import AzimuthalIntegrator
12
+ import fabio
13
+ import numpy as np
14
+
15
+ # pylint: disable=C0103, unused-variable
16
+ def cli():
17
+ """Integrate 2D saxs data"""
18
+ parser = ap.ArgumentParser(
19
+ description="Perform data reduction on data from Xeuss 2.0C")
20
+
21
+ parser.add_argument("sample", help="sample file (.edf)")
22
+
23
+ parser.add_argument("mask", help="mask file (.edf)")
24
+ parser.add_argument("--poni", help="PONI configuration file (.poni)")
25
+ parser.add_argument("--empty", help="empty file (.edf)")
26
+ parser.add_argument("--background", help="background file (.edf)")
27
+ parser.add_argument(
28
+ "--opencl", help="Use OpenCL (requires pyopencl)", action="store_true")
29
+ parser.add_argument(
30
+ "--bkg_factor", help="background correction factor", type=float, default=float(1))
31
+ parser.add_argument(
32
+ "--lac", help="Convert to absolute units using the calculated thickness - LAC in cm^-1",type=float, default=float(0))
33
+ parser.add_argument(
34
+ "--thickness", help="thickness in cm used for transmission correction (otherwise, transmitted intensity used)",type=float, default=float(1))
35
+ parser.add_argument(
36
+ "--autobkg", help="Automatic background scaling", action="store_true")
37
+ args = parser.parse_args()
38
+
39
+
40
+ file = fabio.open(args.sample)
41
+ mask = fabio.open(args.mask)
42
+
43
+
44
+ if args.opencl:
45
+ intmeth = 'opencl'
46
+ else:
47
+ intmeth = 'csr'
48
+ if not args.poni:
49
+ geo = {
50
+ "detector": pyFAI.detectors.Detector(float(file.header['PSize_2']),float(file.header['PSize_1'])),
51
+ "dist": float(file.header['SampleDistance']),
52
+ "poni1": float(file.header['PSize_2'])*float(file.header['Center_2']),
53
+ "poni2": float(file.header['PSize_1'])*float(file.header['Center_1']),
54
+ "rot1": 0,
55
+ "rot2": 0,
56
+ "rot3": 0,
57
+ "wavelength": float(file.header['WaveLength'])
58
+ }
59
+
60
+ # poni = pyFAI.azimuthalIntegrator.AzimuthalIntegrator(**geo)
61
+
62
+ poni = AzimuthalIntegrator(**geo)
63
+ else:
64
+ poni = pyFAI.load(args.poni)
65
+
66
+ if np.allclose(args.lac, 0):
67
+ transmission_sample = args.thickness
68
+ else:
69
+ transmission_sample = -((1/args.lac)*np.log10(float(file.header['Transmission'])))
70
+
71
+ norm_sample = (float(file.header['Intensity1']) *
72
+ float(file.header['ExposureTime']) *
73
+ (
74
+ (
75
+ float(file.header['PSize_1']) /
76
+ float(file.header['SampleDistance']))
77
+ )**2
78
+ )/1e-15
79
+
80
+
81
+ data_1D = poni.integrate1d(file.data,
82
+ 1000, filename=args.sample[:-4]+'_1D.dat', correctSolidAngle=True,
83
+ method=intmeth, radial_range=(None), azimuth_range=(None),
84
+ unit="q_A^-1", mask=mask.data, normalization_factor=norm_sample,
85
+ metadata=None, error_model="poisson")
86
+
87
+ data_2D = file.data/transmission_sample
88
+
89
+ if args.background is not None:
90
+ bkg = fabio.open(args.background)
91
+ if np.allclose(args.lac, 0):
92
+ transmission_bkg = args.thickness
93
+ elif np.allclose(float(bkg.header['Transmission']), 1, atol=5e-3):
94
+ transmission_bkg = -((1/args.lac)*np.log10(1))
95
+ else:
96
+ transmission_bkg = -((1/args.lac)*np.log10(float(bkg.header['Transmission'])))
97
+
98
+ norm_bkg = (float(bkg.header['Intensity1']) *
99
+ float(bkg.header['ExposureTime']) *
100
+ (
101
+ (
102
+ float(bkg.header['PSize_1']) /
103
+ float(bkg.header['SampleDistance']))
104
+ )**2
105
+ )/1e-15
106
+ bkg_1D = poni.integrate1d(bkg.data,
107
+ 1000, filename=args.sample[:-4]+'_bkg1D.dat', correctSolidAngle=True,
108
+ method=intmeth, radial_range=(None), azimuth_range=(None),
109
+ unit="q_A^-1", mask=mask.data, normalization_factor=norm_bkg,
110
+ metadata=None, error_model="poisson")
111
+
112
+ bkg_2D = bkg.data/transmission_bkg
113
+
114
+ if args.autobkg is True:
115
+ print("Use automated background scaling with caution!")
116
+ if max(bkg_1D[1][:np.where(bkg_1D[0] > 0.02)[0][0]]) > max(data_1D[1][:np.where(data_1D[0] > 0.02)[0][0]]):
117
+ bkg_1D = [bkg_1D[0], bkg_1D[1], bkg_1D[2]]
118
+ bkg_1D[1] -= max(bkg_1D[1][:np.where(bkg_1D[0] > 0.02)[0][0]]) - \
119
+ max(data_1D[1][:np.where(data_1D[0] > 0.02)[0][0]])
120
+ else:
121
+ data_I = data_1D[1]
122
+ data_sig = abs(data_1D[2])
123
+ if args.empty is not None:
124
+ empty = fabio.open(args.empty)
125
+ if np.allclose(args.lac, 0):
126
+ transmission_empty = args.thickness
127
+ elif np.allclose(float(bkg.header['Transmission']), 1, atol=5e-3):
128
+ transmission_empty = -((1/args.lac)*np.log10(1))
129
+ else:
130
+ transmission_empty = -((1/args.lac)*np.log10(float(empty.header['Transmission'])))
131
+ norm_empty = (float(empty.header['Intensity1']) *
132
+ float(empty.header['ExposureTime']) *
133
+ (
134
+ (
135
+ float(empty.header['PSize_1']) /
136
+ float(empty.header['SampleDistance']))
137
+ )**2
138
+ )/1e-15
139
+ empty_1D = poni.integrate1d(empty.data,
140
+ 1000, filename=args.sample[:-4]+'_empty1D.dat', correctSolidAngle=True,
141
+ method=intmeth, radial_range=(None), azimuth_range=(None),
142
+ unit="q_A^-1", mask=mask.data, normalization_factor=norm_empty,
143
+ metadata=None, error_model="poisson")
144
+
145
+ empty_2D = empty.data/transmission_empty
146
+ empty_1Df = empty_1D[1]
147
+
148
+ if args.autobkg is True:
149
+ if max(empty_1D[1][:np.where(empty_1D[0] > 0.02)[0][0]]) > max(data_1D[1][:np.where(data_1D[0] > 0.02)[0][0]]):
150
+ empty_1Df = empty_1D[1] - max(empty_1D[1][:np.where(empty_1D[0] > 0.02)[
151
+ 0][0]]) - max(data_1D[1][:np.where(data_1D[0] > 0.02)[0][0]])
152
+
153
+ if args.background is not None:
154
+ data_I = ((data_1D[1]-empty_1Df) -
155
+ (bkg_1D[1]-empty_1Df))*transmission_sample
156
+ data_2I = (data_2D-empty_2D)-(bkg_2D-empty_2D)
157
+ data_sig = (abs(data_1D[2])+abs(empty_1D[2]) +
158
+ abs(bkg_1D[2])+abs(empty_1D[2]))*transmission_sample
159
+ else:
160
+ data_I = (data_1D[1]-empty_1Df)*transmission_sample
161
+ data_2I = data_2D-empty_2D
162
+ data_sig = (abs(data_1D[2])+abs(empty_1D[2]))*transmission_sample
163
+ else:
164
+ if args.background is None:
165
+ data_I = data_1D[1]*transmission_sample
166
+ data_2I = data_2D
167
+ data_sig = abs(data_1D[2])*transmission_sample
168
+ else:
169
+ data_I = (data_1D[1]-bkg_1D[1])*transmission_sample
170
+ data_2I = data_2D - bkg_2D
171
+ data_sig = (abs(data_1D[2])+abs(bkg_1D[2]))*transmission_sample
172
+
173
+ data_2I2 = poni.integrate2d(data_2I, 3000, 3600, unit="q_A^-1")
174
+
175
+ I2, q2, chi2 = data_2I2
176
+
177
+ if np.min(I2) < 0:
178
+ I2min = 1e-3
179
+ else:
180
+ I2min = np.min(I2)
181
+ I2max = np.max(I2)
182
+
183
+ fig, axs = plt.subplot_mosaic(
184
+ [['(a)', '(b)']], figsize=(7, 4), constrained_layout=True)
185
+
186
+ fig1 = axs['(a)'].imshow(I2, origin="lower", extent=[q2.min(), q2.max(), chi2.min(), chi2.max()],
187
+ norm=LogNorm(vmin=I2min, vmax=I2max), cmap=plt.get_cmap('cividis'), aspect="auto")
188
+ divider = make_axes_locatable(axs['(a)'])
189
+ cax = divider.append_axes("top", size="5%", pad=0.05)
190
+
191
+ cbar = plt.colorbar(fig1, cax=cax, orientation="horizontal")
192
+ cbar.ax.tick_params(labelsize=8)
193
+ cax.xaxis.set_ticks_position("top")
194
+
195
+ axs['(a)'].set_xlabel(r'q /$\AA^{-1}$', fontsize=8)
196
+ axs['(a)'].set_ylabel(r'$\phi/^{\circ}$', fontsize=8)
197
+ axs['(a)'].tick_params(labelsize=8)
198
+
199
+ fig2 = axs['(b)'].errorbar(data_1D[0], data_I, yerr=data_sig, linestyle='')
200
+
201
+ axs['(b)'].set_xscale('log')
202
+ axs['(b)'].set_yscale('log')
203
+ axs['(b)'].set_xlabel(r'q /$\AA^{-1}$', fontsize=8)
204
+ if np.allclose(args.lac,0):
205
+ if np.allclose(args.thickness,1):
206
+ axs['(b)'].set_ylabel('I(q) /Arb.', fontsize=8)
207
+
208
+ else:
209
+ axs['(b)'].set_ylabel(r'I(q) /cm$^{-1}$.', fontsize=8)
210
+ axs['(b)'].get_xaxis().set_major_formatter(ticker.ScalarFormatter())
211
+ # axs['(b)'].get_yaxis().set_major_formatter(ticker.ScalarFormatter())
212
+ # axs['(b)'].ticklabel_format(style='sci', axis='both',
213
+ # scilimits=(0, 0), useMathText=True)
214
+ #axs['(b)'].set_xticks([0.01, 0.1, 1])
215
+ # axs['(b)'].get_xaxis().set_major_formatter(ticker.ScalarFormatter())
216
+ axs['(b)'].tick_params(labelsize=8)
217
+ # plt.show()
218
+ plt.savefig(args.sample[:-4]+".png", bbox_inches="tight",dpi=600)
219
+
220
+ data_out = np.column_stack((data_1D[0], data_I, data_sig))
221
+ np.savetxt(args.sample[:-4]+"_subtracted.dat",
222
+ data_out, delimiter='\t', fmt='%s')
223
+
224
+ if __name__ == "__main__":
225
+ cli()
smswaxs/sph_harm.py ADDED
@@ -0,0 +1,160 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Plot spherical harmonics functions from TOPAS coefficients.
4
+
5
+ This module visualizes spherical harmonics functions using coefficients
6
+ obtained from TOPAS academic output files.
7
+ """
8
+
9
+ import click
10
+ import matplotlib.pyplot as plt
11
+ from scipy.special import factorial
12
+ from sympy.functions.special.polynomials import assoc_legendre
13
+ import numpy as np
14
+ from matplotlib import cm, colors
15
+
16
+ # pylint: disable=C0103
17
+
18
+
19
+ def Nlm(l, m):
20
+ """Calculate normalization factor for spherical harmonics."""
21
+ N = np.sqrt(((2*l+1)/(4*np.pi))*(factorial((l-m))/factorial((l+m))))
22
+ return N
23
+
24
+
25
+ def spherical_harmonic(l, m, theta, phi=None):
26
+ """
27
+ Calculate real spherical harmonic Y_lm.
28
+
29
+ Args:
30
+ l: degree of the harmonic
31
+ m: order of the harmonic (0 for m=0, positive for cos, negative for sin)
32
+ theta: polar angle(s)
33
+ phi: azimuthal angle(s), required if m != 0
34
+
35
+ Returns:
36
+ Real spherical harmonic values
37
+ """
38
+ if m == 0:
39
+ return Nlm(l, 0) * assoc_legendre(l, 0, np.cos(theta))
40
+ else:
41
+ abs_m = abs(m)
42
+ legendre_part = assoc_legendre(l, abs_m, np.cos(theta))
43
+ norm = np.sqrt(2) * Nlm(l, abs_m)
44
+ if m > 0: # cosine term (p for plus)
45
+ return norm * np.cos(abs_m * phi) * legendre_part
46
+ else: # sine term (m for minus)
47
+ return norm * np.sin(abs_m * phi) * legendre_part
48
+
49
+
50
+ # Define the coefficient structure: (l, m) pairs
51
+ # m=0 for Y_l0, m>0 for cos terms (p), m<0 for sin terms (m)
52
+ COEFFICIENT_STRUCTURE = [
53
+ (0, 0), # c00
54
+ (2, 0), # c20
55
+ (2, 2), # c22p
56
+ (2, -2), # c22m
57
+ (4, 0), # c40
58
+ (4, 2), # c42p
59
+ (4, -2), # c42m
60
+ (4, 4), # c44p
61
+ (4, -4), # c44m
62
+ (6, 0), # c60
63
+ (6, 2), # c62p
64
+ (6, -2), # c62m
65
+ (6, 4), # c64p
66
+ (6, -4), # c64m
67
+ (6, 6), # c66p
68
+ (6, -6), # c66m
69
+ (8, 0), # c80
70
+ (8, 2), # c82p
71
+ (8, -2), # c82m
72
+ (8, 4), # c84p
73
+ (8, -4), # c84m
74
+ (8, 6), # c86p
75
+ (8, -6), # c86m
76
+ (8, 8), # c88p
77
+ (8, -8), # c88m
78
+ ]
79
+
80
+ @click.command()
81
+ @click.argument('coefficients', nargs=-1, type=float, required=True)
82
+ @click.option('--output', '-o', default='sph_harm.svg',
83
+ help='Output filename for the plot (default: sph_harm.svg)')
84
+ @click.option('--resolution', '-r', default=200,
85
+ help='Angular resolution for the plot (default: 200)')
86
+ def cli(coefficients, output, resolution):
87
+ """
88
+ Plot spherical harmonics functions from TOPAS coefficients.
89
+
90
+ Takes 1 to 25 coefficients and generates a 3D visualization of the spherical
91
+ harmonics function. Coefficients should be provided in order:
92
+ c00, c20, c22p, c22m, c40, c42p, c42m, c44p, c44m, c60, c62p, c62m, c64p, c64m,
93
+ c66p, c66m, c80, c82p, c82m, c84p, c84m, c86p, c86m, c88p, c88m
94
+
95
+ If fewer than 25 coefficients are provided, the remaining ones are assumed to be zero.
96
+ This allows for 2nd order (5 coeffs), 4th order (9 coeffs), 6th order (16 coeffs), etc.
97
+
98
+ Use '--' before coefficients if any are negative to prevent them being interpreted as options.
99
+
100
+ Examples:
101
+ # 2nd order (5 coefficients)
102
+ sm-spharm -- 1 -0.54889 0.48504 0.74711 0.79916
103
+
104
+ # 8th order (25 coefficients)
105
+ sm-spharm -- 1 -0.54889 0.48504 0.74711 0.79916 -0.33178 0.16323 0.45369 \
106
+ -0.29782 -0.29214 -0.43949 0.01524 0.09042 0.85340 0.40316 \
107
+ -0.01881 1.47673 -0.55751 -0.68970 0.02621 -0.06565 -0.56472 \
108
+ 1.00981 -0.12339 -0.25831
109
+ """
110
+ # Validate number of coefficients
111
+ if len(coefficients) > 25:
112
+ raise click.BadParameter(
113
+ f'Too many coefficients: got {len(coefficients)}, maximum is 25'
114
+ )
115
+
116
+ # Pad coefficients with zeros if fewer than 25 provided
117
+ coefficients = list(coefficients) + [0.0] * (25 - len(coefficients))
118
+
119
+ # Create angular grid
120
+ phi, theta = np.mgrid[0:2*np.pi:complex(0, resolution), 0:np.pi:complex(0, resolution//2)]
121
+
122
+ # Calculate radial function R by summing all harmonics
123
+ R = np.zeros_like(theta, dtype=float)
124
+
125
+ for (l_val, m_val), coeff in zip(COEFFICIENT_STRUCTURE, coefficients):
126
+ if m_val == 0:
127
+ # For m=0, only theta is needed
128
+ y_lm_func = np.frompyfunc(lambda t, lv=l_val, mv=m_val: spherical_harmonic(lv, mv, t), 1, 1)
129
+ y_values = y_lm_func(theta).astype(float)
130
+ else:
131
+ # For m!=0, both theta and phi are needed
132
+ y_lm_func = np.frompyfunc(lambda t, p, lv=l_val, mv=m_val: spherical_harmonic(lv, mv, t, p), 2, 1)
133
+ y_values = y_lm_func(theta, phi).astype(float)
134
+
135
+ R += coeff * y_values
136
+
137
+ # Convert to Cartesian coordinates
138
+ x = R*np.sin(theta)*np.cos(phi)
139
+ y = R*np.sin(theta)*np.sin(phi)
140
+ z = R*np.cos(theta)
141
+
142
+ # Create 3D plot
143
+ # pylint: disable=E1101
144
+ _, ax = plt.subplots(subplot_kw=dict(projection='3d'), figsize=(14, 10))
145
+ norm = colors.Normalize()
146
+ ax.plot_surface(x, y, z, rstride=1, cstride=1, facecolors=cm.jet(norm(R)))
147
+
148
+ ax.set_xlabel('X')
149
+ ax.set_ylabel('Y')
150
+ ax.set_zlabel('Z')
151
+ ax.set_title('Spherical Harmonics Visualization')
152
+
153
+ # Save the plot
154
+ plt.savefig(output, bbox_inches='tight')
155
+ click.echo(f'Plot saved to {output}')
156
+
157
+
158
+ if __name__ == "__main__":
159
+ # pylint: disable=E1120
160
+ cli()
smswaxs/topas_init.py ADDED
@@ -0,0 +1,24 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Created on Wed Dec 20 11:38:05 2023
4
+
5
+ @author: u0092172
6
+ """
7
+
8
+ import sys
9
+ def cli():
10
+ if len(sys.argv) != 2:
11
+ print("Usage: python topas_init.py <input_file>")
12
+ sys.exit(1)
13
+ with open(sys.argv[1],'r',encoding="utf-8") as inputfile:
14
+ data = inputfile.readlines()
15
+
16
+ for i, line in enumerate(data):
17
+ if 'xdd' in line:
18
+ data[i] = 'xdd '+sys.argv[1][:-3]+'xye\n'
19
+
20
+ with open(sys.argv[1], 'w',encoding="utf-8") as outfile:
21
+ outfile.writelines(data)
22
+
23
+ if __name__ == "__main__":
24
+ cli()
@@ -0,0 +1,239 @@
1
+ """
2
+ This script will integrate 2D SAXS data collected on the Xenocs
3
+ Xeuss SWAXS instrument and subtract background data if necessary.
4
+ """
5
+ import argparse as ap
6
+ import matplotlib.ticker as ticker
7
+ import matplotlib.pyplot as plt
8
+ from matplotlib.colors import LogNorm
9
+ from mpl_toolkits.axes_grid1 import make_axes_locatable
10
+ import pyFAI
11
+ from pyFAI.integrator.azimuthal import AzimuthalIntegrator
12
+ import fabio
13
+ import numpy as np
14
+
15
+ # pylint: disable=C0103, unused-variable
16
+
17
+
18
+ def cli():
19
+ """Integrate 2D saxs data"""
20
+ parser = ap.ArgumentParser(
21
+ description="Perform data reduction on data from Xeuss 2.0C")
22
+
23
+ parser.add_argument("sample", help="sample file (.edf)")
24
+
25
+ parser.add_argument("mask", help="mask file (.edf)")
26
+ parser.add_argument("--poni", help="PONI configuration file (.poni)")
27
+ parser.add_argument("--background", help="background file (.edf)")
28
+ parser.add_argument(
29
+ "--opencl", help="Use OpenCL (requires pyopencl)", action="store_true")
30
+ parser.add_argument(
31
+ "--bkg_factor", help="background correction factor", type=float, default=float(1))
32
+ parser.add_argument(
33
+ "--lac", help="Convert to absolute units using the calculated thickness - LAC in cm^-1",
34
+ type=float, default=float(0))
35
+ parser.add_argument(
36
+ "--thickness", help="thickness /cm used for trans. cor. (otherwise, trans. int. used)",
37
+ type=float, default=float(0))
38
+ parser.add_argument(
39
+ "--xmin", help="Minimum value of radial unit (q)", type=float)
40
+ parser.add_argument(
41
+ "--xmax", help="Max value of radial unit (q)", type=float, default=3.8)
42
+ parser.add_argument(
43
+ "--raw2D", help="Display the 2D image in the lab frame", action="store_true")
44
+ parser.add_argument(
45
+ "--noabs", help="No absorption correction", action="store_true")
46
+ parser.add_argument(
47
+ "--unit", help="Radial unit for integration, q or 2th", type=str, default="q")
48
+ args = parser.parse_args()
49
+
50
+ if args.unit.lower() == "q":
51
+ UNIT = "q_A^-1"
52
+
53
+ elif args.unit.lower() == "2th":
54
+ UNIT = "2th_deg"
55
+ else:
56
+ raise ValueError("Unit must be 'q' or '2th'")
57
+ if args.xmin:
58
+ radran = (args.xmin, args.xmax)
59
+ else:
60
+ radran = (None)
61
+ file = fabio.open(args.sample)
62
+ mask = fabio.open(args.mask)
63
+ if args.noabs is False:
64
+ def abscor(tth, intensity, trans):
65
+ """Absorption correction for WAXS according to the Bragg corrections section of the
66
+ XSACT user manual (2020)"""
67
+ if np.allclose(trans, 1, atol=5e-3):
68
+ return intensity
69
+ else:
70
+ fabs = (np.log(trans)*((1/np.cos(np.radians(tth)))-1)) / \
71
+ (np.exp(np.log(trans)*((1/np.cos(np.radians(tth)))-1))-1)
72
+ cor_int = intensity/fabs
73
+ return cor_int
74
+ else:
75
+ def abscor(_, intensity, __):
76
+ return intensity
77
+
78
+ if args.opencl:
79
+ intmeth = 'opencl'
80
+ else:
81
+ intmeth = 'csr'
82
+ if not args.poni:
83
+ geo = {
84
+ "detector": pyFAI.detectors.Detector(float(file.header['PSize_2']),
85
+ float(file.header['PSize_1'])),
86
+ "dist": float(file.header['SampleDistance']),
87
+ "poni1": float(file.header['PSize_2'])*float(file.header['Center_2']),
88
+ "poni2": float(file.header['PSize_1'])*float(file.header['Center_1']),
89
+ "rot1": 0,
90
+ "rot2": 0,
91
+ "rot3": 0,
92
+ "wavelength": float(file.header['WaveLength'])
93
+ }
94
+
95
+ # poni = pyFAI.azimuthalIntegrator.AzimuthalIntegrator(**geo)
96
+
97
+ poni = AzimuthalIntegrator(**geo)
98
+ else:
99
+ poni = pyFAI.load(args.poni)
100
+ try:
101
+ transmission_sample = float(file.header['Transmission'])
102
+ except KeyError:
103
+ if np.allclose(args.lac, 0):
104
+ args.noabs = True
105
+ transmission_sample = 1
106
+ else:
107
+ if np.allclose(args.thickness, 0) is False:
108
+ transmission_sample = np.exp(-args.lac*args.thickness)
109
+ else:
110
+ args.noabs = True
111
+ transmission_sample = 1
112
+ norm_sample = (float(file.header['Intensity1']) *
113
+ float(file.header['ExposureTime']) *
114
+ (
115
+ (
116
+ float(file.header['PSize_1']) /
117
+ float(file.header['SampleDistance']))
118
+ )**2
119
+ )/1e-15
120
+
121
+ data_1D = poni.integrate1d(file.data,
122
+ 1000, filename=args.sample[:-4]+'_1D.dat', correctSolidAngle=True,
123
+ method=intmeth, radial_range=radran, azimuth_range=(None),
124
+ unit=UNIT, mask=mask.data, normalization_factor=norm_sample,
125
+ metadata=None, error_model="poisson")
126
+
127
+ data_1D_corr = [data_1D[0], data_1D[1], data_1D[2]]
128
+ if args.unit.lower() == "2th":
129
+ data_1D_corr[1] = abscor(data_1D[0], data_1D[1], transmission_sample)
130
+ else:
131
+ data_1D_corr[1] = abscor(2*np.degrees(np.arcsin(((float(file.header['WaveLength'])/1e-10)
132
+ * data_1D[0])/(4*np.pi))), data_1D[1], transmission_sample)
133
+ data_2D = file.data/transmission_sample
134
+
135
+ if args.background is not None:
136
+ bkg = fabio.open(args.background)
137
+ if np.allclose(float(bkg.header['Transmission']), 1, atol=5e-3):
138
+ transmission_bkg = 1
139
+ else:
140
+ transmission_bkg = float(bkg.header['Transmission'])
141
+
142
+ norm_bkg = (float(bkg.header['Intensity1']) *
143
+ float(bkg.header['ExposureTime']) *
144
+ (
145
+ (
146
+ float(bkg.header['PSize_1']) /
147
+ float(bkg.header['SampleDistance']))
148
+ )**2
149
+ )/1e-15
150
+ bkg_1D = poni.integrate1d(bkg.data,
151
+ 1000, filename=args.sample[:-4]+'_bkg1D.dat',
152
+ correctSolidAngle=True, method=intmeth,
153
+ radial_range=radran, azimuth_range=(None),
154
+ unit=UNIT, mask=mask.data, normalization_factor=norm_bkg,
155
+ metadata=None, error_model="poisson")
156
+
157
+ bkg_1D_corr = [bkg_1D[0], bkg_1D[1], bkg_1D[2]]
158
+ if args.unit.lower() == "2th":
159
+ bkg_1D_corr[1] = abscor(bkg_1D[0], bkg_1D[1], transmission_bkg)
160
+ else:
161
+ bkg_1D_corr[1] = abscor(
162
+ 2*np.degrees(np.arcsin(((float(file.header['WaveLength'])/1e-10)*bkg_1D[0])/
163
+ (4*np.pi))), bkg_1D[1], transmission_bkg)
164
+ bkg_2D = bkg.data/transmission_bkg * args.bkg_factor
165
+ data_I = data_1D_corr[1]-bkg_1D_corr[1]
166
+ data_2I = data_2D - bkg_2D
167
+ data_sig = abs(data_1D_corr[2])+abs(bkg_1D_corr[2])
168
+ else:
169
+ data_I = data_1D_corr[1]
170
+ data_2I = data_2D
171
+ data_sig = data_1D_corr[2]
172
+
173
+ data_2I2 = poni.integrate2d(data_2I, 3000, 3600, unit=UNIT)
174
+
175
+ I2, q2, chi2 = data_2I2
176
+
177
+ if np.min(I2) < 0:
178
+ I2min = 1e-3
179
+ else:
180
+ I2min = np.min(I2)
181
+ I2max = np.max(I2)
182
+
183
+ fig, axs = plt.subplot_mosaic(
184
+ [['(a)', '(b)']], figsize=(7, 4), constrained_layout=True)
185
+ if args.raw2D is False:
186
+ fig1 = axs['(a)'].imshow(I2, origin="lower",
187
+ extent=[q2.min(), q2.max(), chi2.min(), chi2.max()],
188
+ norm=LogNorm(vmin=I2min, vmax=I2max), cmap=plt.get_cmap('cividis'),
189
+ aspect="auto")
190
+ if args.unit.lower() == "q":
191
+ axs['(a)'].set_xlabel(r'q /$\AA^{-1}$', fontsize=8)
192
+ else:
193
+ axs['(a)'].set_xlabel(r'2$\theta$ /$^{\circ}$', fontsize=8)
194
+ axs['(a)'].set_ylabel(r'$\phi/^{\circ}$', fontsize=8)
195
+ else:
196
+ fig1 = axs['(a)'].imshow(data_2I, norm=LogNorm(
197
+ vmin=I2min, vmax=I2max), cmap=plt.get_cmap('cividis'), aspect="auto")
198
+ axs['(a)'].set_xlabel('X /px', fontsize=8)
199
+ axs['(a)'].set_ylabel('Z /px', fontsize=8)
200
+ divider = make_axes_locatable(axs['(a)'])
201
+ cax = divider.append_axes("top", size="5%", pad=0.05)
202
+
203
+ cbar = plt.colorbar(fig1, cax=cax, orientation="horizontal")
204
+ cbar.ax.tick_params(labelsize=8)
205
+ cax.xaxis.set_ticks_position("top")
206
+
207
+ axs['(a)'].tick_params(labelsize=8)
208
+
209
+ fig2 = axs['(b)'].plot(data_1D[0], data_I, linewidth=0.5)
210
+
211
+ # axs['(b)'].set_xscale('log')
212
+ # axs['(b)'].set_yscale('log')
213
+ if args.unit.lower() == "q":
214
+ axs['(b)'].set_xlabel(r'q /$\AA^{-1}$', fontsize=8)
215
+ else:
216
+ axs['(b)'].set_xlabel(r'2$\theta$ /$^{\circ}$', fontsize=8)
217
+ axs['(b)'].set_ylabel('I(q) /Arb.', fontsize=8)
218
+ axs['(b)'].get_xaxis().set_major_formatter(ticker.ScalarFormatter())
219
+ # axs['(b)'].get_yaxis().set_major_formatter(ticker.ScalarFormatter())
220
+ # axs['(b)'].ticklabel_format(style='sci', axis='both',
221
+ # scilimits=(0, 0), useMathText=True)
222
+ # axs['(b)'].set_xticks([0.01, 0.1, 1])
223
+ # axs['(b)'].get_xaxis().set_major_formatter(ticker.ScalarFormatter())
224
+ axs['(b)'].tick_params(labelsize=8)
225
+ # plt.show()
226
+ plt.savefig(args.sample[:-4]+".png", bbox_inches="tight",dpi=600)
227
+
228
+ data_out = np.column_stack((data_1D[0], data_I, data_sig))
229
+ if args.unit.lower() == "q":
230
+ header_str = "q(A^-1)\tI(Arb.)\tSigma(Arb.)"
231
+ np.savetxt(args.sample[:-4]+"_subtracted.dat",
232
+ data_out, delimiter='\t', fmt='%s', header=header_str)
233
+ elif args.unit.lower() == "2th":
234
+ np.savetxt(args.sample[:-4]+".xye",
235
+ data_out, delimiter='\t', fmt='%s')
236
+
237
+
238
+ if __name__ == "__main__":
239
+ cli()
@@ -0,0 +1,48 @@
1
+ """This script converts XSACT or Foxtrot dat files to HDF5 format for archival storage"""
2
+ import sys
3
+ import re
4
+ from pathlib import Path
5
+ from concurrent.futures import ThreadPoolExecutor
6
+ import h5py
7
+ import numpy as np
8
+
9
+ def get_line_number(phrase: str, file_name: str):
10
+ """Determine the line in the text file containing the specified str"""
11
+ with open(file_name, encoding="utf-8") as f:
12
+ for i, line in enumerate(f, 1):
13
+ p = re.compile(phrase)
14
+ if p.match(line) is not None:
15
+ return i
16
+
17
+ def process_dat_file(file_path: str, hdf):
18
+ """determine the header and data block in the dat file and create an
19
+ hdf5 dataset"""
20
+ dataset_name = file_path.stem
21
+ with open(file_path, encoding="utf-8") as infile:
22
+ lines = infile.read().splitlines()
23
+ lines = [line.replace('##', ' ') for line in lines]
24
+ meta = {s.split(" ")[0].replace("# ", "").strip(): s.split(
25
+ " ")[-1].strip() for s in lines if '#' in s}
26
+ meta = {key.replace("#",""): value.replace("#","") for key,value in meta.items()}
27
+ meta = {key: value for key, value in meta.items() if key}
28
+ head = get_line_number('.*q\\([AÅ].*',file_path)
29
+ meta["columns"] = lines[head-1].split()
30
+ data = np.genfromtxt(file_path,skip_header=head,usecols=(0,1,2),encoding="utf-8")
31
+
32
+ dset = hdf.create_dataset(
33
+ dataset_name, data=data, compression='gzip')
34
+
35
+ for attr_name, attr_value in meta.items():
36
+ dset.attrs[attr_name] = attr_value
37
+
38
+ def add_dats_to_hdf5(directory: str, hdf5_filename: str):
39
+ """find all dat files in the current folder and subfolders, then
40
+ add them to a single hdf5 file"""
41
+ files = list(Path(directory).glob('**/*.dat'))
42
+ hdf = h5py.File(hdf5_filename,'w')
43
+ with ThreadPoolExecutor() as executor:
44
+ executor.map(lambda file: process_dat_file(file, hdf), files)
45
+ hdf.close()
46
+
47
+ if __name__ == '__main__':
48
+ add_dats_to_hdf5(sys.argv[1],sys.argv[2])
@@ -0,0 +1,39 @@
1
+ """This script converts Xeuss edf files to HDF5 format for archival storage"""
2
+ import argparse as ap
3
+ from pathlib import Path
4
+ from concurrent.futures import ThreadPoolExecutor
5
+ import fabio
6
+ import h5py
7
+
8
+
9
+
10
+ def process_edf_file(file_path: Path, hdf):
11
+ """Create hdf5 dataset from edf file"""
12
+ infile = fabio.open(file_path)
13
+ dataset_name = file_path.stem
14
+
15
+ dset = hdf.create_dataset(
16
+ dataset_name, data=infile.data, compression='gzip')
17
+
18
+ for attr_name, attr_value in infile.header.items():
19
+ dset.attrs[attr_name] = attr_value
20
+
21
+ def add_edfs_to_hdf5():
22
+ """Find all edf files in all subdirectories of the specified
23
+ path and add them to the hdf5"""
24
+ parser = ap.ArgumentParser(description="Convert Xeuss edf files to HDF5 format")
25
+ parser.add_argument("directory", help="Directory containing edf files")
26
+ args = parser.parse_args()
27
+ if ".edf" in args.directory:
28
+ files = [Path(args.directory)]
29
+ args.hdf5_filename = files[0].with_suffix('.h5').name
30
+ else:
31
+ files = list(Path(args.directory).glob('**/*.edf'))
32
+ args.hdf5_filename = Path(args.directory).name + '.h5'
33
+ hdf = h5py.File(args.hdf5_filename,'w')
34
+ with ThreadPoolExecutor() as executor:
35
+ executor.map(lambda file: process_edf_file(file, hdf), files)
36
+ hdf.close()
37
+
38
+ if __name__ == '__main__':
39
+ add_edfs_to_hdf5()
@@ -0,0 +1,26 @@
1
+ """This script converts Xeuss edf files to TIFF format + JSON for archival storage"""
2
+ import sys
3
+ from pathlib import Path
4
+ import json
5
+ import fabio
6
+ from PIL import Image
7
+
8
+ file_path = Path(sys.argv[1])
9
+
10
+ infile = fabio.open(file_path)
11
+
12
+ output_path = file_path.with_suffix(".tif")
13
+ json_output = file_path.with_suffix(".json")
14
+
15
+ """The original edf files contain 32-bit floats, so the F mode is used to
16
+ retain precision, however, mask files are only 8-bit and will throw an error,
17
+ in this case we fall back to auto detection"""
18
+ try:
19
+ image = Image.fromarray(infile.data,mode='F')
20
+ except ValueError:
21
+ image = Image.fromarray(infile.data)
22
+
23
+ image.save(output_path,format='TIFF',compression='tiff_deflate')
24
+
25
+ with open(json_output, 'w',encoding="utf-8") as json_f:
26
+ json.dump(infile.header, json_f, indent=4)
smswaxs/xrdml_conv.py ADDED
@@ -0,0 +1,48 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ Convert xrdml files to xy format for use in TOPAS. Extract metadata, but drop units from pint quantities.
4
+ @author: u0092172
5
+ """
6
+ from pathlib import Path
7
+ import json
8
+ import argparse as ap
9
+ import numpy as np
10
+ from fairmat_readers_xrd import read_file
11
+
12
+
13
+ def _to_magnitude(obj):
14
+ """Recursively convert pint quantities to their magnitudes."""
15
+ if hasattr(obj, 'magnitude'):
16
+ return obj.magnitude
17
+ if isinstance(obj, dict):
18
+ return {k: _to_magnitude(v) for k, v in obj.items()}
19
+ if isinstance(obj, (list, tuple)):
20
+ converted = [_to_magnitude(v) for v in obj]
21
+ return tuple(converted) if isinstance(obj, tuple) else converted
22
+ return obj
23
+
24
+ def cli():
25
+ """Convert xrdml files to xy format for use in TOPAS"""
26
+ parser = ap.ArgumentParser(description='Sort out xrdml files')
27
+ parser.add_argument("input", help="xrdml file")
28
+ args = parser.parse_args() # map arguments to args
29
+
30
+
31
+
32
+ data = read_file(args.input)
33
+ two_theta = data['2Theta'].magnitude
34
+ intensity = data['intensity'].magnitude
35
+
36
+ metadata = _to_magnitude(data['metadata'])
37
+
38
+ outfile = Path(args.input).with_suffix('.xy')
39
+
40
+ np.savetxt(outfile, np.column_stack((two_theta, intensity)), fmt='%1.6f', delimiter='\t')
41
+
42
+ meta_outfile = Path(args.input).with_suffix('.json')
43
+ with open(meta_outfile, 'w') as f:
44
+ json.dump(metadata, f, indent=4)
45
+
46
+
47
+ if __name__ == '__main__':
48
+ cli()
@@ -0,0 +1,173 @@
1
+ Metadata-Version: 2.3
2
+ Name: SusMat-SWAXS
3
+ Version: 2.1.3
4
+ Summary: A collection of python scripts for uploading analytical data to iRODS
5
+ Author: Samuel Eyley
6
+ Author-email: Samuel Eyley <susmat-swaxspypi.xepsj@simplelogin.com>
7
+ Classifier: Programming Language :: Python :: 3
8
+ Classifier: License :: OSI Approved :: MIT License
9
+ Classifier: Operating System :: OS Independent
10
+ Requires-Dist: pyfai>2024.10.0
11
+ Requires-Dist: pyopencl
12
+ Requires-Dist: click
13
+ Requires-Dist: pyside6
14
+ Requires-Dist: h5py
15
+ Requires-Dist: matplotlib
16
+ Requires-Dist: numpy
17
+ Requires-Dist: scipy
18
+ Requires-Dist: sympy
19
+ Requires-Dist: fabio
20
+ Requires-Dist: hdf5plugin
21
+ Requires-Dist: pillow
22
+ Requires-Dist: fairmat-readers-xrd>=0.0.8
23
+ Requires-Python: >=3.12
24
+ Project-URL: Homepage, https://github.com/Sustainable-Materials-Lab/SusMat-SWAXS
25
+ Project-URL: Issues, https://github.com/Sustainable-Materials-Lab/SusMat-SWAXS/issues
26
+ Description-Content-Type: text/markdown
27
+
28
+ # SWAXS tools
29
+
30
+ ## Description
31
+ This is a collection of scripts to perform data conversion and integration on X-ray scattering data, specifically for data collected on the Xenocs Xeuss SWAXS instrument.
32
+
33
+ **Available tools:**
34
+ - `sm-datconv` - Converts Xenocs format 1D data (.dat) to TOPAS compatible .xye format
35
+ - `sm-edfh5` - Convert EDF files to HDF5 format with metadata preservation
36
+ - `sm-saxsint` - Integrate 2D SAXS data from Xenocs Xeuss instruments with background subtraction
37
+ - `sm-waxsint` - Integrate 2D WAXS data from Xenocs Xeuss instruments with absorption correction
38
+ - `sm-spharm` - Visualize spherical harmonics functions from TOPAS coefficients
39
+
40
+ ## Installation
41
+ Install in a virtual environment to avoid dependency conflicts:
42
+
43
+ ```bash
44
+ pip install SusMat-SWAXS
45
+ ```
46
+
47
+
48
+ ## Usage
49
+ All scripts are available as command-line tools after installation. Use the `--help` flag with any command to see detailed usage information.
50
+
51
+ ### sm-datconv
52
+ Converts Xenocs format 1D data (.dat) to TOPAS compatible .xye format.
53
+
54
+ **Input:** `.dat` file with tab-separated columns for q, I(q) (and optionally sigma).
55
+
56
+ **Usage:**
57
+ ```bash
58
+ sm-datconv <filename_without_extension>
59
+ ```
60
+
61
+ **Output:** `.xye` file with tab-separated columns for 2θ, I, and error (if present in input).
62
+
63
+ **Batch processing example (PowerShell):**
64
+ ```powershell
65
+ Get-ChildItem -Filter *.dat | ForEach-Object -Process {sm-datconv $_.BaseName}
66
+ ```
67
+
68
+ ### sm-xrdmlconv
69
+ Converts Malvern PANalytical format 1D data (.xrdml) to TOPAS compatible .xy format.
70
+
71
+ **Input:** `.xrdml` file .
72
+
73
+ **Usage:**
74
+ ```bash
75
+ sm-xrdmlconv <filename_without_extension>
76
+ ```
77
+
78
+ **Output:** `.xy` file with tab-separated columns for 2θ and I and a `.json` file containing metadata.
79
+
80
+ ### sm-edfh5
81
+ Convert multiple EDF files to a single HDF5 file with metadata preservation.
82
+
83
+ **Usage:**
84
+ ```bash
85
+ sm-edfh5 <path to directory containing edf files>
86
+ ```
87
+
88
+ ### sm-saxsint
89
+ Integrate 2D SAXS data from Xenocs Xeuss instruments with background subtraction and absorption correction.
90
+
91
+ **Usage:**
92
+ ```bash
93
+ sm-saxsint <sample.edf> <mask.edf> [OPTIONS]
94
+ ```
95
+
96
+ **Key options:**
97
+ - `--poni <file.poni>` - PONI calibration file
98
+ - `--background <bkg.edf>` - Background file for subtraction
99
+ - `--bkg_factor <float>` - Background scaling factor (default: 1.0)
100
+ - `--unit <q|2th>` - Radial unit for integration (default: q)
101
+ - `--xmin <float>` - Minimum q value (default: 0.004)
102
+ - `--xmax <float>` - Maximum q value (default: 0.7)
103
+ - `--opencl` - Use OpenCL acceleration (requires pyopencl)
104
+ - `--raw2D` - Display 2D image in lab frame
105
+
106
+ **Output:**
107
+ - `*_1D.dat` - Integrated 1D data
108
+ - `*_subtracted.dat` - Background-subtracted data
109
+ - `*.svg` - Visualization plot
110
+
111
+ ### sm-waxsint
112
+ Integrate 2D WAXS data from Xenocs Xeuss instruments with absorption correction.
113
+
114
+ **Usage:**
115
+ ```bash
116
+ sm-waxsint <sample.edf> <mask.edf> [OPTIONS]
117
+ ```
118
+
119
+ **Key options:**
120
+ - `--poni <file.poni>` - PONI calibration file
121
+ - `--background <bkg.edf>` - Background file for subtraction
122
+ - `--bkg_factor <float>` - Background scaling factor (default: 1.0)
123
+ - `--unit <q|2th>` - Radial unit for integration (default: q)
124
+ - `--xmin <float>` - Minimum q value (default: 0.36)
125
+ - `--xmax <float>` - Maximum q value (default: 3.8)
126
+ - `--lac <float>` - Linear absorption coefficient in cm⁻¹ for thickness correction
127
+ - `--thickness <float>` - Sample thickness in cm for transmission correction
128
+ - `--noabs` - Disable absorption correction
129
+ - `--opencl` - Use OpenCL acceleration (requires pyopencl)
130
+ - `--raw2D` - Display 2D image in lab frame
131
+
132
+ **Output:**
133
+ - `*_1D.dat` - Integrated 1D data
134
+ - `*_subtracted.dat` - Background-subtracted, absorption-corrected data
135
+ - `*.svg` - Visualization plot
136
+
137
+ ### sm-spharm
138
+ Visualize spherical harmonics functions using coefficients from TOPAS academic output files.
139
+
140
+ **Usage:**
141
+ ```bash
142
+ sm-spharm C00 C20 C22P C22M C40 C42P C42M C44P C44M C60 C62P C62M C64P C64M C66P C66M C80 C82P C82M C84P C84M C86P C86M C88P C88M [OPTIONS]
143
+ ```
144
+
145
+ **Arguments:** 25 spherical harmonics coefficients (c00 through c88m) from TOPAS output file
146
+
147
+ **Options:**
148
+ - `--output <filename>` / `-o <filename>` - Output filename for the plot (default: sph_harm.svg)
149
+ - `--resolution <int>` / `-r <int>` - Angular resolution for the plot (default: 200)
150
+
151
+ **Example:**
152
+ ```bash
153
+ sm-spharm 1 -0.54889 0.48504 0.74711 0.79916 -0.33178 0.16323 0.45369 \
154
+ -0.29782 -0.29214 -0.43949 0.01524 0.09042 0.85340 0.40316 \
155
+ -0.01881 1.47673 -0.55751 -0.68970 0.02621 -0.06565 -0.56472 \
156
+ 1.00981 -0.12339 -0.25831 --output my_spherical_harmonics.svg
157
+ ```
158
+
159
+ **Output:** 3D visualization plot showing the spherical harmonics function in real space
160
+
161
+ ## Support
162
+ Submit an issue on github: https://github.com/Sustainable-Materials-Lab/SusMat-SWAXS
163
+
164
+ ## Contributing
165
+ Contact Samuel Eyley if you are interested in contributing.
166
+
167
+ ## Authors and acknowledgment
168
+ Originally authored by Samuel Eyley.
169
+
170
+ Significant portions of code were developed with the help of Github Copilot (2025).
171
+
172
+ ## License
173
+ MIT License
@@ -0,0 +1,15 @@
1
+ smswaxs/__init__.py,sha256=kXT2_MXyf8y-GSFimdeq6rMi8To60hKqiSIyhrS0-Pk,962
2
+ smswaxs/dat_conv.py,sha256=MuHSmV9Yd9pJVAkkLOo7sVzlmgwXneAjYauhxnuurMo,4373
3
+ smswaxs/saxs_integration.py,sha256=qNNPoK26n0qS0ZsmMzhLINkpR9BzDkksc2VGfWeE4MI,6472
4
+ smswaxs/saxs_integration_xeuss.py,sha256=WJu4IvkqY1LfcGokT-KIP0wNxKvRN47AmOy34vo-0OI,9764
5
+ smswaxs/sph_harm.py,sha256=PKpSpLVXYoCjLWPb6bzzXPTpnheQ3LCL6jHd6KDuVeA,5554
6
+ smswaxs/topas_init.py,sha256=5zRSGK0Qz8cqFdcL9EJQ9Oe-q7BjBe_EByCW2TikQbI,563
7
+ smswaxs/waxs_integration_xeuss.py,sha256=a9-3vNAFH8dAHc0PyFD-oT4ksdjYABsfJoJvuhFj6ps,9925
8
+ smswaxs/xeuss_dat_h5_multi.py,sha256=U2BY8fh0tEXmIWewfthzqltT1aEIeluoJGXg6b7lTr0,1979
9
+ smswaxs/xeuss_edf_h5_multi.py,sha256=kiG8qG7LSlHm7d6sCCgC-lkSRwSykUa-BIYsSplp31Q,1370
10
+ smswaxs/xeuss_edf_tiff.py,sha256=25XxAk9o8C3zV--pu8EyMtfQXbqdRymYUcdKFL3fsHE,807
11
+ smswaxs/xrdml_conv.py,sha256=yM0h037OqX2AxMdim6UUUAD3AUCc7M7hL-yRaglYmuA,1441
12
+ susmat_swaxs-2.1.3.dist-info/WHEEL,sha256=M4DeIjVCA49okfALADZoWX5JOGwnmHb-JOpQHtI-1c0,80
13
+ susmat_swaxs-2.1.3.dist-info/entry_points.txt,sha256=Jbp7q-UnvB425gUtK4fx7uUDUg2M1zhs-RrDLzVi5kw,313
14
+ susmat_swaxs-2.1.3.dist-info/METADATA,sha256=wovQaqxLi5iqSX6aOCZj-0Lhx-zHpYTkh17zx-DQijo,5888
15
+ susmat_swaxs-2.1.3.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: uv 0.11.2
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,9 @@
1
+ [console_scripts]
2
+ sm-datconv = smswaxs.dat_conv:cli
3
+ sm-edfh5 = smswaxs.xeuss_edf_h5_multi:add_edfs_to_hdf5
4
+ sm-saxsint = smswaxs.saxs_integration_xeuss:cli
5
+ sm-spharm = smswaxs.sph_harm:cli
6
+ sm-topasinit = smswaxs.topas_init:cli
7
+ sm-waxsint = smswaxs.waxs_integration_xeuss:cli
8
+ sm-xrdmlconv = smswaxs.xrdml_conv:cli
9
+