• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

xraysoftmat / kkcalc / 20025982922

08 Dec 2025 11:06AM UTC coverage: 32.87% (-0.5%) from 33.398%
20025982922

Pull #19

github

web-flow
Merge 88fa50712 into 122d5226d
Pull Request #19: ci: Add Semantic Versioning and Changelog

102 of 221 new or added lines in 19 files covered. (46.15%)

14 existing lines in 7 files now uncovered.

1373 of 4177 relevant lines covered (32.87%)

1.23 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/kkcalc/asf_database/db_generator_script.py
1
"""
2
This module accumulates optical database data.
3

4
This file is not included in the distributed package, but is provided
5
for reference.
6

7
Data is taken from  different sources and packaged into `ASF.json` to be
8
used by and distributed with the Kramers-Kronig Calculator software package.
9

10
Workflow to accomodate:
11
1. Read data from .nff files and BL files.
12
2. Combine Henke and BL data sets.
13
3. Convert to useful format for internal use.
14
4. Write to json file for distribution.
15
5. Load data for use by KKcalc.
16
6. Combine data for different elements as selected by user.
17
6.a) figure out energy values (abscissa).
18
6.b) add coefficients/intensity values in selected proportions
19
7. Provide combined data in required formats.
20
7.a) list of tuples for plotting.
21
7.b) list of energy ranges, each with corresponding list of polynomial coefficients, (i.e. piecewise polynomial format) for PP KK calculation.
22

23
Items 1-4 not usually performed by users. Items 5-7 must be integrated into KKcalc program.
24
"""
25

NEW
26
import os
×
NEW
27
import os.path
×
28
import scipy.interpolate
×
29
import json
×
30
import numpy.typing as npt
×
31
import numpy as np
×
NEW
32
import gzip
×
NEW
33
import pkgutil
×
NEW
34
import io
×
35

36
BASEDIR = os.path.dirname(os.path.realpath(__file__))
×
37
classical_electron_radius = 2.81794029957951365441605230194258e-15  # meters
×
38
Plancks_constant = 4.1356673310e-15  # eV*seconds
×
39
speed_of_light = 2.99792458e8  # meters per second
×
40
Avogadro_constant = 6.02214129e23
×
41

NEW
42
try:
×
NEW
43
    data_resource = pkgutil.get_data(
×
44
        "kkcalc",
45
        "asf_database/db_data/elements.dat",
46
    )
NEW
47
    with io.BytesIO(data_resource) as f:
×
NEW
48
        Elements_DATA = [line.decode("utf-8").strip("\r\n").split() for line in f]
×
NEW
49
except IOError:
×
50
    # Try a relative path if pkgutil fails (e.g. when running the script directly)
NEW
51
    Elements_DATA = [
×
52
        line.strip("\r\n").split()
53
        for line in open(os.path.join(BASEDIR, "db_data", "elements.dat"))
54
    ]
UNCOV
55
Database = dict()
×
56

57

58
#################################################################################################################
59
def LoadData(filename: str) -> npt.NDArray:
×
60
    """
61
    A loader for the Henke .nff ascii data files.
62

63
    Parameters
64
    ----------
65
    filename : str
66
        The path to the .nff file to load.
67

68
    Returns
69
    -------
70
    npt.NDArray
71
        The loaded floating data as a numpy array.
72
    """
73
    data = []
×
74
    if os.path.isfile(filename):
×
75
        for line in open(filename):
×
76
            try:
×
77
                data.append([float(f) for f in line.split()])
×
78
            except ValueError:
×
79
                pass
×
80
        data = np.array(data)
×
81
    else:
82
        print("Error:", filename, "is not a valid file name.")
×
83
    if len(data) == 0:
×
84
        print("Error: no data found in", filename)
×
85
    return np.array(data)
×
86

87

NEW
88
def parse_BL_file(briggs_file: str | io.BytesIO) -> dict[int, npt.NDArray]:
×
89
    """
90
    Parse a Biggs and Lighthill (BL) file.
91

92
    Parameters
93
    ----------
94
    briggs_file : str
95
        The path to the BL file to parse.
96

97
    Returns
98
    -------
99
    dict[int, npt.NDArray]
100
        A dictionary containing the parsed data.
101
        The keys are element atomic numbers, and the values are numpy arrays of coefficients.
102

103
    Raises
104
    ------
105
    FileNotFoundError
106
        If the specified BL file does not exist.
107
    """
108
    continue_norm = True  # Normalise the Biggs and Lighthill data as the published scattering factors do, rather than as Henke et al says.
×
109
    BLfile = {}
×
110
    # Read the file
NEW
111
    if isinstance(briggs_file, str):
×
NEW
112
        if os.path.exists(briggs_file) is False:
×
NEW
113
            raise FileNotFoundError(
×
114
                f"Biggs and Lighthill file not found: {briggs_file}"
115
            )
NEW
116
        with open(briggs_file, "r") as f:
×
NEW
117
            lines = f.readlines()
×
NEW
118
    elif isinstance(briggs_file, io.BytesIO):
×
NEW
119
        briggs_file.seek(0)  # Ensure we're at the start of the BytesIO stream
×
NEW
120
        lines = briggs_file.readlines()
×
NEW
121
        lines = [line.decode("utf-8") for line in lines]
×
122
    else:
NEW
123
        raise TypeError("briggs_file must be a string path or a BytesIO object.")
×
124
    # Process the lines
NEW
125
    for line in lines:
×
126
        try:
×
127
            values = [float(f) for f in line.split()]
×
128
            if values[3] > 10:
×
129
                Norm_value = 0  # will calculate actual normalisation value later
×
130
                if (
×
131
                    not continue_norm
132
                    and values[2] > 10
133
                    and values[2] not in [20, 100, 500, 100000]
134
                ):
135
                    Norm_value = 1
×
136
                elif (
×
137
                    not continue_norm
138
                    and values[0] == 42
139
                    and values[2] > 10
140
                    and values[2] not in [100, 500, 100000]
141
                ):  # Mo needs special handling
142
                    # print "Mo seen at", values[0], values[2]
143
                    Norm_value = 1
×
144
                values.append(Norm_value)
×
145
                if values[2] not in [0.01, 0.1, 0.8, 4, 20, 100, 500, 100000] or (
×
146
                    values[0] == 42 and values[2] == 20
147
                ):
148
                    values.append(1)  # this is an absorption edge!
×
149
                else:
150
                    values.append(0)  # this is not an absorption edge
×
151
                BLfile[int(values[0])].append(values)
×
152
        except ValueError:
×
153
            pass
×
154
        except IndexError:
×
155
            pass
×
156
        except KeyError:
×
157
            BLfile[int(values[0])] = [values]
×
158
    for elem, coeffs in list(BLfile.items()):
×
159
        BLfile[elem] = np.array(coeffs)[:, 2:]
×
160
    return BLfile
×
161

162

163
def BL_to_ASF(E: npt.ArrayLike, coeffs: npt.NDArray, Atomic_mass: float) -> npt.NDArray:
×
164
    r"""
165
    The conversion factor from Biggs and Lighthill coefficients to Henke scattering factors.
166

167
    Biggs and Lighthill offers photoelectric cross-section (PECS) with the sum of
168

169
    ..math::
170
        PECS = \sum_{n=1}^{4} A_n * E^{-n}
171

172
    where n is the reciprocal order (n=1-4), with E in keV and PECS in cm^2/g.
173
    The Henke scattering factors are related by
174

175
    ..math::
176
        f2 = PECS*E/(2*r0*h*c),
177

178
    where E is the energy (eV), PECS cm^2/atom.
179

180
    Parameters
181
    ----------
182
    E : npt.ArrayLike
183
        The energies to calculate the Henke scattering factors at.
184
        Can be singular or an array of energies.
185
    coeffs : npt.NDArray
186
        The polynomial coefficients corresponding to the energies.
187
        Should have a shape of at least (4,), but another dimension
188
        can match the shape of E for vectorised calculations.
189
    Atomic_mass : float
190
        The atomic mass of the element being calculated (in atomic mass units).
191

192
    Returns
193
    -------
194
    npt.NDArray
195
        The f2 Henke scattering factors.
196
    """
197
    # If E is not a singular value, convert to array for vectorised calculation
198
    if not isinstance(E, (int, float)):
×
199
        E = np.asarray(E)
×
200
    return (
×
201
        (
202
            coeffs[0]
203
            + coeffs[1] / (E * 0.001)
204
            + coeffs[2] / ((E * 0.001) ** 2)
205
            + coeffs[3] / ((E * 0.001) ** 3)
206
        )
207
        * Atomic_mass
208
        / (
209
            2
210
            * Avogadro_constant
211
            * classical_electron_radius
212
            * Plancks_constant
213
            * speed_of_light
214
        )
215
        * 0.1
216
    )
217

218

219
def Coeffs_to_ASF(E: npt.ArrayLike, coeffs: npt.NDArray) -> npt.NDArray:
×
220
    r"""
221
    Calculate Henke scattering factors from polynomial coefficients.
222

223
    Uses the linear n=1, n=0 coefficients from Henke data, and the
224
    n = -1, -2, -3 coefficients from Biggs and Lighthill data.
225

226
    ..math::
227
        f2 = \sum_{n=0}^{4} B_n * E^{1-n}
228

229
    E in eV and PECS in cm^2/atom
230

231
    Parameters
232
    ----------
233
    E : npt.ArrayLike
234
        The energies to calculate the Henke scattering factors at.
235
        Can be singular or an array of energies.
236
    coeffs : npt.NDArray
237
        The polynomial coefficients corresponding to the energies.
238
        Should have a shape of at least (5,), with coefficients
239
        ordered from n=1 to n=-3, but another dimension can match
240
        the shape of E for vectorised calculations.
241

242
    Returns
243
    -------
244
    npt.NDArray
245
        The f2 Henke scattering factors.
246
    """
247
    if not isinstance(E, (int, float)):
×
248
        E = np.asarray(E)
×
249
    return (
×
250
        coeffs[0] * E
251
        + coeffs[1]
252
        + coeffs[2] / E
253
        + coeffs[3] / (E**2)
254
        + coeffs[4] / (E**3)
255
    )
256

257

258
###########################################################################################################
259

NEW
260
data = pkgutil.get_data(
×
261
    "kkcalc",
262
    "asf_database/db_data/original_biggs_file.dat",
263
)
NEW
264
if data is not None:
×
NEW
265
    file = io.BytesIO(data)
×
266
else:
267
    # Try a relative path if pkgutil fails (e.g. when running the script directly)
NEW
268
    file = os.path.join(BASEDIR, "db_data", "original_biggs_file.dat")
×
NEW
269
    if not os.path.exists(file):
×
NEW
270
        raise FileNotFoundError("Biggs and Lighthill file not found in package data.")
×
271

NEW
272
BL_data = parse_BL_file(briggs_file=file)
×
273

274
# for z, symbol, name, atomic_mass, Henke_file in [Elements_DATA[0]]:
275
for z, symbol, name, atomic_mass, Henke_file in Elements_DATA:
×
276
    # print(z, symbol, name, atomic_mass, Henke_file)
277
    # Get basic metadata
278
    Element_Database = dict()
×
279
    Element_Database["mass"] = float(atomic_mass)
×
280
    Element_Database["name"] = name
×
281
    Element_Database["symbol"] = symbol
×
282

283
    # Get basic data
284
    # print("Load nff data from:", os.path.join(BASEDIR, 'data', Henke_file))
NEW
285
    data_path = os.path.join(BASEDIR, "db_data", Henke_file)
×
286
    asf_RawData = LoadData(data_path)
×
287
    if min(asf_RawData[1:-1, 0] - asf_RawData[0:-2, 0]) < 0:
×
288
        print(
×
289
            "Warning! Energies in ",
290
            Henke_file,
291
            "are not in ascending order! (Sorting now..)",
292
        )
293
        asf_RawData.sort()
×
294
    # print BL_data[int(z)]
295

296
    # Convert and normalise BL data
297
    # get normalisation values
298
    ASF_norm = scipy.interpolate.splev(
×
299
        10000,
300
        scipy.interpolate.splrep(asf_RawData[:, 0], asf_RawData[:, 2], k=1),
301
        der=0,
302
    )
303
    BL_norm = BL_to_ASF(10000, BL_data[int(z)][0][3:7], float(atomic_mass))
×
304
    # print "Norms:", ASF_norm, BL_norm, BL_norm/ASF_norm
305

306
    temp_E = []
×
307
    BL_coefficients = []
×
308
    for line in BL_data[int(z)]:
×
309
        if float(line[1]) >= 30:
×
310
            temp_E.append(float(line[0]))
×
311
            BL_coefficients.append(
×
312
                line[2:7]
313
                / BL_norm
314
                * ASF_norm
315
                * [0, 1, 1000, 1000000, 1000000000]
316
                * float(atomic_mass)
317
                / (
318
                    2
319
                    * Avogadro_constant
320
                    * classical_electron_radius
321
                    * Plancks_constant
322
                    * speed_of_light
323
                )
324
                * 0.1
325
            )
326
    # store for use in calculation
327
    C = np.array(BL_coefficients)
×
328
    # (insert 30000.1 here to use linear section from 30000 to 30000.2 to ensure continuity between data sets)
329
    X = np.array([30.0001] + temp_E[1:]) * 1000
×
330

331
    # Express asf data in PP
332
    M = (asf_RawData[1:, 2] - asf_RawData[0:-1, 2]) / (
×
333
        asf_RawData[1:, 0] - asf_RawData[0:-1, 0]
334
    )
335
    B = asf_RawData[0:-1, 2] - M * asf_RawData[0:-1, 0]
×
336
    E = asf_RawData[:, 0]
×
337
    # asf_RawData (i.e. E, Re, Im) matches dimensions at this stage. Energies only go to 30000 eV, so we need to extend them. Briggs Lighthill adds 4 points.
338

339
    Full_coeffs = np.zeros((len(asf_RawData[:, 0]) - 1, 5))
×
340
    Full_coeffs[:, 0] = M
×
341
    Full_coeffs[:, 1] = B
×
342
    # Append B&L data and make sure it is continuous
343
    E = E[0:-1]
×
344
    for i in range(len(X) - 1):
×
345
        Y1 = Coeffs_to_ASF(X[i] - 0.1, Full_coeffs[-1, :])
×
346
        Y2 = Coeffs_to_ASF(X[i] + 0.1, C[i, :])
×
347
        M = (Y2 - Y1) / 0.2
×
348
        B = Y1 - M * (X[i] - 0.1)
×
349
        E = np.append(E, [X[i] - 0.1, X[i] + 0.1])
×
350
        Full_coeffs = np.append(Full_coeffs, [[M, B, 0, 0, 0]], axis=0)
×
351
        Full_coeffs = np.append(Full_coeffs, [C[i, :]], axis=0)
×
352
    E = np.append(E, X[-1])
×
353

354
    # Store -9999. Re values as np.nan instead
355
    asf_RawData[asf_RawData[:, 1] == -9999.0, 1] = np.nan
×
356

357
    # convert np arrays to nested lists to enable json serialisation with the default converter.
358
    Element_Database["E"] = E.tolist()
×
359
    Element_Database["Im"] = Full_coeffs.tolist()
×
360
    Element_Database["Re"] = asf_RawData[:, 1].tolist()
×
361
    Database[int(z)] = Element_Database
×
362

363
output_path = os.path.join(BASEDIR, "ASF.json")
×
364
with open(output_path, "w") as f:
×
365
    json.dump(Database, f, indent=None)  # Indent: 1 for readability, None for compact.
×
366

367
# Compress the database file
368
with open(output_path, "rb") as f_in:
×
UNCOV
369
    with gzip.open(output_path + ".gz", "wb") as f_out:
×
370
        f_out.writelines(f_in)
×
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc