pdfbl.sequential 0.1.0rc0__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.
- pdfbl/__init__.py +14 -0
- pdfbl/sequential/__init__.py +23 -0
- pdfbl/sequential/pdfadapter.py +448 -0
- pdfbl/sequential/pdfbl_sequential_app.py +25 -0
- pdfbl/sequential/sequential_cmi_runner.py +557 -0
- pdfbl/sequential/version.py +26 -0
- pdfbl_sequential-0.1.0rc0.dist-info/METADATA +182 -0
- pdfbl_sequential-0.1.0rc0.dist-info/RECORD +13 -0
- pdfbl_sequential-0.1.0rc0.dist-info/WHEEL +5 -0
- pdfbl_sequential-0.1.0rc0.dist-info/entry_points.txt +2 -0
- pdfbl_sequential-0.1.0rc0.dist-info/licenses/AUTHORS.rst +10 -0
- pdfbl_sequential-0.1.0rc0.dist-info/licenses/LICENSE.rst +29 -0
- pdfbl_sequential-0.1.0rc0.dist-info/top_level.txt +1 -0
pdfbl/__init__.py
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
##############################################################################
|
|
3
|
+
#
|
|
4
|
+
# (c) 2025 The Trustees of Columbia University in the City of New York.
|
|
5
|
+
# All rights reserved.
|
|
6
|
+
#
|
|
7
|
+
# File coded by: Billinge Group members and community contributors.
|
|
8
|
+
#
|
|
9
|
+
# See GitHub contributions for a more detailed list of contributors.
|
|
10
|
+
# https://github.com/pdf-bl/pdfbl.sequential/graphs/contributors
|
|
11
|
+
#
|
|
12
|
+
# See LICENSE.rst for license information.
|
|
13
|
+
#
|
|
14
|
+
##############################################################################
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
##############################################################################
|
|
3
|
+
#
|
|
4
|
+
# (c) 2025 Simon Billinge.
|
|
5
|
+
# All rights reserved.
|
|
6
|
+
#
|
|
7
|
+
# File coded by: members of the Billinge Group and PDF beamline at NSLS-II.
|
|
8
|
+
#
|
|
9
|
+
# See GitHub contributions for a more detailed list of contributors.
|
|
10
|
+
# https://github.com/pdf-bl/pdfbl.sequential/graphs/contributors
|
|
11
|
+
#
|
|
12
|
+
# See LICENSE.rst for license information.
|
|
13
|
+
#
|
|
14
|
+
##############################################################################
|
|
15
|
+
"""Automated sequential refinements of PDF data."""
|
|
16
|
+
|
|
17
|
+
# package version
|
|
18
|
+
from pdfbl.sequential.version import __version__ # noqa
|
|
19
|
+
|
|
20
|
+
# silence the pyflakes syntax checker
|
|
21
|
+
assert __version__ or True
|
|
22
|
+
|
|
23
|
+
# End of file
|
|
@@ -0,0 +1,448 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import tempfile
|
|
3
|
+
import warnings
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from queue import Queue
|
|
6
|
+
from typing import Literal
|
|
7
|
+
|
|
8
|
+
import numpy
|
|
9
|
+
from diffpy.srfit.fitbase import (
|
|
10
|
+
FitContribution,
|
|
11
|
+
FitRecipe,
|
|
12
|
+
FitResults,
|
|
13
|
+
Profile,
|
|
14
|
+
)
|
|
15
|
+
from diffpy.srfit.pdf import PDFGenerator, PDFParser
|
|
16
|
+
from diffpy.srfit.structure import constrainAsSpaceGroup
|
|
17
|
+
from diffpy.structure.parsers import getParser
|
|
18
|
+
from scipy.optimize import least_squares
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class PDFAdapter:
|
|
22
|
+
"""Adapter to expose PDF fitting interface. Designed to provide a
|
|
23
|
+
simplified PDF fitting interface for human users and AI agents.
|
|
24
|
+
|
|
25
|
+
Attributes
|
|
26
|
+
----------
|
|
27
|
+
recipe : FitRecipe
|
|
28
|
+
The FitRecipe object managing the fitting process.
|
|
29
|
+
|
|
30
|
+
Methods
|
|
31
|
+
-------
|
|
32
|
+
initialize_profile(profile_path, qmin=None, qmax=None, xmin=None, xmax=None, dx=None)
|
|
33
|
+
Load and initialize the PDF profile from the given file path with
|
|
34
|
+
some optional parameters.
|
|
35
|
+
initialize_structures(structure_paths : list[str], run_parallel=True)
|
|
36
|
+
Load and initialize the structures from the given file paths, and
|
|
37
|
+
generate corresponding PDFGenerator objects.
|
|
38
|
+
initialize_contribution(equation_string=None)
|
|
39
|
+
Initialize the FitContribution object combining the PDF generators and
|
|
40
|
+
the profile.
|
|
41
|
+
initialize_recipe()
|
|
42
|
+
Initialize the FitRecipe object for the fitting process.
|
|
43
|
+
set_initial_variable_values(variable_name_to_value : dict)
|
|
44
|
+
Update parameter values from the provided dictionary.
|
|
45
|
+
refine_variables(variable_names: list[str])
|
|
46
|
+
Refine the parameters specified in the list and in that order.
|
|
47
|
+
get_variable_names()
|
|
48
|
+
Get the names of all variables in the recipe.
|
|
49
|
+
save_results(mode: str, filename: str=None)
|
|
50
|
+
Save the fitting results.
|
|
51
|
+
""" # noqa: E501
|
|
52
|
+
|
|
53
|
+
def __init__(self):
|
|
54
|
+
self.intermediate_results = {}
|
|
55
|
+
self.iter_count = 0
|
|
56
|
+
|
|
57
|
+
def monitor_intermediate_results(
|
|
58
|
+
self, key: str, step: int = 10, queue: Queue = None
|
|
59
|
+
):
|
|
60
|
+
"""Store an intermediate result during the fitting process.
|
|
61
|
+
|
|
62
|
+
Parameters
|
|
63
|
+
----------
|
|
64
|
+
key : str
|
|
65
|
+
The key to identify the intermediate result.
|
|
66
|
+
step : int
|
|
67
|
+
The step interval to store the intermediate result.
|
|
68
|
+
queue : Queue
|
|
69
|
+
The queue to store the intermediate results.
|
|
70
|
+
"""
|
|
71
|
+
if queue is None:
|
|
72
|
+
queue = Queue()
|
|
73
|
+
self.intermediate_results[(key, step)] = queue
|
|
74
|
+
|
|
75
|
+
def initialize_profile(
|
|
76
|
+
self,
|
|
77
|
+
profile_path: str,
|
|
78
|
+
qmin=None,
|
|
79
|
+
qmax=None,
|
|
80
|
+
xmin=None,
|
|
81
|
+
xmax=None,
|
|
82
|
+
dx=None,
|
|
83
|
+
):
|
|
84
|
+
"""Load and initialize the PDF profile from the given file path
|
|
85
|
+
with some optional parameters.
|
|
86
|
+
|
|
87
|
+
The target output, FitRecipe, requires a profile object, multiple
|
|
88
|
+
PDFGenerator objects, and a FitContribution object combining them. This
|
|
89
|
+
method initializes the profile object.
|
|
90
|
+
|
|
91
|
+
Parameters
|
|
92
|
+
----------
|
|
93
|
+
profile_path : str
|
|
94
|
+
The path to the experimental PDF profile file.
|
|
95
|
+
qmin : float
|
|
96
|
+
The minimum Q value for PDF calculation. The default value is
|
|
97
|
+
the one parsed from the profile file.
|
|
98
|
+
qmax : float
|
|
99
|
+
The maximum Q value for PDF calculation. The default value is the
|
|
100
|
+
one parsed from the profile file.
|
|
101
|
+
xmin : float
|
|
102
|
+
The minimum r value for PDF calculation. The default value is the
|
|
103
|
+
one parsed from the profile file.
|
|
104
|
+
xmax : float
|
|
105
|
+
The maximum r value for PDF calculation. The default value is the
|
|
106
|
+
one parsed from the profile file.
|
|
107
|
+
dx : float
|
|
108
|
+
The r step size for PDF calculation. The default value is the
|
|
109
|
+
one parsed from the profile file.
|
|
110
|
+
"""
|
|
111
|
+
profile = Profile()
|
|
112
|
+
parser = PDFParser()
|
|
113
|
+
parser.parseString(Path(profile_path).read_text())
|
|
114
|
+
profile.loadParsedData(parser)
|
|
115
|
+
if qmin:
|
|
116
|
+
profile.meta["qmin"] = qmin
|
|
117
|
+
if qmax:
|
|
118
|
+
profile.meta["qmax"] = qmax
|
|
119
|
+
profile.setCalculationRange(xmin=xmin, xmax=xmax, dx=dx)
|
|
120
|
+
self.profile = profile
|
|
121
|
+
|
|
122
|
+
def initialize_structures(
|
|
123
|
+
self, structure_paths: list[str], run_parallel=True
|
|
124
|
+
):
|
|
125
|
+
"""Load and initialize the structures from the given file paths,
|
|
126
|
+
and generate corresponding PDFGenerator objects.
|
|
127
|
+
|
|
128
|
+
The target output, FitRecipe, requires a profile object, multiple
|
|
129
|
+
PDFGenerator objects, and a FitContribution object combining them. This
|
|
130
|
+
method creates the PDFGenerator objects from the structure files.
|
|
131
|
+
|
|
132
|
+
Must be called after initialize_profile.
|
|
133
|
+
|
|
134
|
+
Parameters
|
|
135
|
+
----------
|
|
136
|
+
structure_paths : list of str
|
|
137
|
+
The list of paths to the structure files (CIF format).
|
|
138
|
+
|
|
139
|
+
Notes
|
|
140
|
+
-----
|
|
141
|
+
Planned features:
|
|
142
|
+
- Support cif file manipulation.
|
|
143
|
+
- Add/Remove atoms.
|
|
144
|
+
- symmetry operations?
|
|
145
|
+
"""
|
|
146
|
+
if isinstance(structure_paths, str):
|
|
147
|
+
structure_paths = [structure_paths]
|
|
148
|
+
structures = []
|
|
149
|
+
spacegroups = []
|
|
150
|
+
pdfgenerators = []
|
|
151
|
+
if run_parallel:
|
|
152
|
+
try:
|
|
153
|
+
import multiprocessing
|
|
154
|
+
from multiprocessing import Pool
|
|
155
|
+
|
|
156
|
+
import psutil
|
|
157
|
+
|
|
158
|
+
syst_cores = multiprocessing.cpu_count()
|
|
159
|
+
cpu_percent = psutil.cpu_percent()
|
|
160
|
+
avail_cores = numpy.floor(
|
|
161
|
+
(100 - cpu_percent) / (100.0 / syst_cores)
|
|
162
|
+
)
|
|
163
|
+
ncpu = int(numpy.max([1, avail_cores]))
|
|
164
|
+
pool = Pool(processes=ncpu)
|
|
165
|
+
self.pool = pool
|
|
166
|
+
except ImportError:
|
|
167
|
+
warnings.warn(
|
|
168
|
+
"\nYou don't appear to have the necessary packages for "
|
|
169
|
+
"parallelization. Proceeding without parallelization."
|
|
170
|
+
)
|
|
171
|
+
run_parallel = False
|
|
172
|
+
for i, structure_path in enumerate(structure_paths):
|
|
173
|
+
stru_parser = getParser("cif")
|
|
174
|
+
structure = stru_parser.parse(Path(structure_path).read_text())
|
|
175
|
+
sg = getattr(stru_parser, "spacegroup", None)
|
|
176
|
+
spacegroup = sg.short_name if sg is not None else "P1"
|
|
177
|
+
structures.append(structure)
|
|
178
|
+
spacegroups.append(spacegroup)
|
|
179
|
+
pdfgenerator = PDFGenerator(f"G{i+1}")
|
|
180
|
+
pdfgenerator.setStructure(structure)
|
|
181
|
+
if run_parallel:
|
|
182
|
+
pdfgenerator.parallel(ncpu=ncpu, mapfunc=self.pool.map)
|
|
183
|
+
pdfgenerators.append(pdfgenerator)
|
|
184
|
+
self.spacegroups = spacegroups
|
|
185
|
+
self.pdfgenerators = pdfgenerators
|
|
186
|
+
|
|
187
|
+
def initialize_contribution(self, equation_string=None):
|
|
188
|
+
"""Initialize the FitContribution object combining the PDF
|
|
189
|
+
generators and the profile.
|
|
190
|
+
|
|
191
|
+
The target output, FitRecipe, requires a profile object, multiple
|
|
192
|
+
PDFGenerator objects, and a FitContribution object combining them. This
|
|
193
|
+
method creates the FitContribution object combining the profile and PDF
|
|
194
|
+
generators.
|
|
195
|
+
|
|
196
|
+
Must be called after initialize_profile and initialize_structures.
|
|
197
|
+
|
|
198
|
+
Parameters
|
|
199
|
+
----------
|
|
200
|
+
equation_string : str
|
|
201
|
+
The equation string defining the contribution. The default
|
|
202
|
+
equation will be generated based on the number of phases.
|
|
203
|
+
e.g.
|
|
204
|
+
for one phase: "s0*G1",
|
|
205
|
+
for two phases: "s0*(s1*G1+(1-s1)*G2)",
|
|
206
|
+
for three phases: "s0*(s1*G1+s2*G2+(1-(s1+s2))*G3)",
|
|
207
|
+
...
|
|
208
|
+
|
|
209
|
+
Notes
|
|
210
|
+
-----
|
|
211
|
+
Planned features:
|
|
212
|
+
- Support registerFunction for custom equations.
|
|
213
|
+
"""
|
|
214
|
+
contribution = FitContribution("pdfcontribution")
|
|
215
|
+
contribution.setProfile(self.profile)
|
|
216
|
+
for pdfgenerator in self.pdfgenerators:
|
|
217
|
+
contribution.addProfileGenerator(pdfgenerator)
|
|
218
|
+
number_of_phase = len(self.pdfgenerators)
|
|
219
|
+
if equation_string is None:
|
|
220
|
+
if number_of_phase == 1:
|
|
221
|
+
equation_string = "s0*G1"
|
|
222
|
+
else:
|
|
223
|
+
equation_string = (
|
|
224
|
+
"s0*("
|
|
225
|
+
+ "+".join(
|
|
226
|
+
[f"s{i+1}*G{i+1}" for i in range(number_of_phase - 1)]
|
|
227
|
+
)
|
|
228
|
+
+ f"+(1-({'+'.join([f's{i+1}' for i in range(1, number_of_phase)])}))*G{number_of_phase}" # noqa: E501
|
|
229
|
+
+ ")"
|
|
230
|
+
)
|
|
231
|
+
contribution.setEquation(equation_string)
|
|
232
|
+
self.contribution = contribution
|
|
233
|
+
return self.contribution
|
|
234
|
+
|
|
235
|
+
def initialize_recipe(
|
|
236
|
+
self,
|
|
237
|
+
):
|
|
238
|
+
"""Initialize the FitRecipe object for the fitting process.
|
|
239
|
+
|
|
240
|
+
The target output, FitRecipe, requires a profile object, multiple
|
|
241
|
+
PDFGenerator objects, and a FitContribution object combining them. This
|
|
242
|
+
method creates the FitRecipe object combining the profile, PDF
|
|
243
|
+
generators, and contribution.
|
|
244
|
+
|
|
245
|
+
Must be called after initialize_contribution.
|
|
246
|
+
|
|
247
|
+
Notes
|
|
248
|
+
-----
|
|
249
|
+
Planned features:
|
|
250
|
+
- support instructions to
|
|
251
|
+
- add variables
|
|
252
|
+
- constrain variables of the scatters
|
|
253
|
+
- change symmetry constraints
|
|
254
|
+
"""
|
|
255
|
+
recipe = FitRecipe()
|
|
256
|
+
recipe.addContribution(self.contribution)
|
|
257
|
+
qdamp = recipe.newVar("qdamp", fixed=False, value=0.04)
|
|
258
|
+
qbroad = recipe.newVar("qbroad", fixed=False, value=0.02)
|
|
259
|
+
for i, (pdfgenerator, spacegroup) in enumerate(
|
|
260
|
+
zip(self.pdfgenerators, self.spacegroups)
|
|
261
|
+
):
|
|
262
|
+
for pname in [
|
|
263
|
+
"delta1",
|
|
264
|
+
"delta2",
|
|
265
|
+
]:
|
|
266
|
+
par = getattr(pdfgenerator, pname)
|
|
267
|
+
recipe.addVar(par, name=pname + f"_{i+1}", fixed=False)
|
|
268
|
+
if len(self.pdfgenerators) > 1:
|
|
269
|
+
recipe.addVar(
|
|
270
|
+
getattr(self.contribution, f"s{i+1}"),
|
|
271
|
+
name=f"s{i+1}",
|
|
272
|
+
fixed=False,
|
|
273
|
+
)
|
|
274
|
+
recipe.restrain(f"s{i+1}", lb=0.0, ub=1.0)
|
|
275
|
+
recipe.constrain(pdfgenerator.qdamp, qdamp)
|
|
276
|
+
recipe.constrain(pdfgenerator.qbroad, qbroad)
|
|
277
|
+
stru_parset = pdfgenerator.phase
|
|
278
|
+
spacegroupparams = constrainAsSpaceGroup(stru_parset, spacegroup)
|
|
279
|
+
for par in spacegroupparams.xyzpars:
|
|
280
|
+
recipe.addVar(par, name=par.name + f"_{i+1}", fixed=False)
|
|
281
|
+
for par in spacegroupparams.latpars:
|
|
282
|
+
recipe.addVar(par, name=par.name + f"_{i+1}", fixed=False)
|
|
283
|
+
for par in spacegroupparams.adppars:
|
|
284
|
+
recipe.addVar(par, name=par.name + f"_{i+1}", fixed=False)
|
|
285
|
+
recipe.addVar(self.contribution.s0, name="s0", fixed=False)
|
|
286
|
+
recipe.fix("all")
|
|
287
|
+
recipe.fithooks[0].verbose = 0
|
|
288
|
+
self.recipe = recipe
|
|
289
|
+
|
|
290
|
+
def set_initial_variable_values(self, variable_name_to_value: dict):
|
|
291
|
+
"""Update parameter values from the provided dictionary.
|
|
292
|
+
|
|
293
|
+
Parameters
|
|
294
|
+
----------
|
|
295
|
+
variable_name_to_value : dict
|
|
296
|
+
A dictionary mapping variable names to their new values.
|
|
297
|
+
"""
|
|
298
|
+
for vname, vvalue in variable_name_to_value.items():
|
|
299
|
+
self.recipe._parameters[vname].setValue(vvalue)
|
|
300
|
+
|
|
301
|
+
def residual(self, p=[]):
|
|
302
|
+
"""Wrapper for the recipe residual function to store
|
|
303
|
+
intermediate results if needed.
|
|
304
|
+
|
|
305
|
+
Parameters
|
|
306
|
+
----------
|
|
307
|
+
p : list
|
|
308
|
+
List of parameter values.
|
|
309
|
+
|
|
310
|
+
Returns
|
|
311
|
+
-------
|
|
312
|
+
numpy.ndarray
|
|
313
|
+
The residual array.
|
|
314
|
+
"""
|
|
315
|
+
residual = self.recipe.residual(p)
|
|
316
|
+
if self.intermediate_results is not None:
|
|
317
|
+
fitresults = FitResults(self.recipe)
|
|
318
|
+
for (key, step), values in self.intermediate_results.items():
|
|
319
|
+
if (self.iter_count % step) == 0:
|
|
320
|
+
value = getattr(fitresults, key)
|
|
321
|
+
values.put(value)
|
|
322
|
+
self.iter_count += 1
|
|
323
|
+
return residual
|
|
324
|
+
|
|
325
|
+
def refine_variables(self, variable_names: list[str]):
|
|
326
|
+
"""Refine the parameters specified in the list and in that
|
|
327
|
+
order. Must be called after initialize_recipe.
|
|
328
|
+
|
|
329
|
+
Parameters
|
|
330
|
+
----------
|
|
331
|
+
variable_names : list of str
|
|
332
|
+
The names of the variables to refine.
|
|
333
|
+
"""
|
|
334
|
+
for vname in variable_names:
|
|
335
|
+
if vname not in self.recipe._parameters:
|
|
336
|
+
raise ValueError(
|
|
337
|
+
f"Variable {vname} not found in the recipe. "
|
|
338
|
+
"Please choose from the existing variables: "
|
|
339
|
+
f"{list(self.recipe._parameters.keys())}"
|
|
340
|
+
)
|
|
341
|
+
for vname in variable_names:
|
|
342
|
+
self.recipe.free(vname)
|
|
343
|
+
least_squares(
|
|
344
|
+
self.residual,
|
|
345
|
+
self.recipe.values,
|
|
346
|
+
x_scale="jac",
|
|
347
|
+
)
|
|
348
|
+
|
|
349
|
+
def get_variable_names(self) -> list[str]:
|
|
350
|
+
"""Get the names of all variables in the recipe.
|
|
351
|
+
|
|
352
|
+
Returns
|
|
353
|
+
-------
|
|
354
|
+
list of str
|
|
355
|
+
A list of variable names.
|
|
356
|
+
"""
|
|
357
|
+
return list(self.recipe._parameters.keys())
|
|
358
|
+
|
|
359
|
+
def save_results(
|
|
360
|
+
self, mode: Literal["str", "dict"] = "str", filename=None
|
|
361
|
+
):
|
|
362
|
+
"""Save the fitting results. Must be called after
|
|
363
|
+
refine_variables.
|
|
364
|
+
|
|
365
|
+
Parameters
|
|
366
|
+
----------
|
|
367
|
+
mode : str
|
|
368
|
+
The format to save the results. Options are:
|
|
369
|
+
"str" - Save results as a formatted text string.
|
|
370
|
+
"dict" - Save results as a JSON-compatible dictionary.
|
|
371
|
+
filename : str
|
|
372
|
+
The path to the output file. If None, results will not be saved to
|
|
373
|
+
a file.
|
|
374
|
+
|
|
375
|
+
Returns
|
|
376
|
+
-------
|
|
377
|
+
str or dict
|
|
378
|
+
The fitting results in the specified format.
|
|
379
|
+
"""
|
|
380
|
+
fit_results = FitResults(self.recipe)
|
|
381
|
+
if mode == "str":
|
|
382
|
+
if filename is None:
|
|
383
|
+
tmp_directory = tempfile.TemporaryDirectory()
|
|
384
|
+
temp_file = Path(tmp_directory.name) / "data.txt"
|
|
385
|
+
filename = str(temp_file)
|
|
386
|
+
fit_results.saveResults(filename)
|
|
387
|
+
with open(filename, "r") as f:
|
|
388
|
+
results_str = f.read()
|
|
389
|
+
if filename is None:
|
|
390
|
+
tmp_directory.cleanup()
|
|
391
|
+
return results_str
|
|
392
|
+
|
|
393
|
+
elif mode == "dict":
|
|
394
|
+
results_dict = {}
|
|
395
|
+
results_dict["residual"] = fit_results.residual
|
|
396
|
+
results_dict["contributions"] = (
|
|
397
|
+
fit_results.residual - fit_results.penalty
|
|
398
|
+
)
|
|
399
|
+
results_dict["restraints"] = fit_results.penalty
|
|
400
|
+
results_dict["chi2"] = fit_results.chi2
|
|
401
|
+
results_dict["reduced_chi2"] = fit_results.rchi2
|
|
402
|
+
results_dict["rw"] = fit_results.rw
|
|
403
|
+
# variables
|
|
404
|
+
results_dict["variables"] = {}
|
|
405
|
+
for name, val, unc in zip(
|
|
406
|
+
fit_results.varnames, fit_results.varvals, fit_results.varunc
|
|
407
|
+
):
|
|
408
|
+
results_dict["variables"][name] = {
|
|
409
|
+
"value": val,
|
|
410
|
+
"uncertainty": unc,
|
|
411
|
+
}
|
|
412
|
+
# fixed variables
|
|
413
|
+
results_dict["fixed_variables"] = {}
|
|
414
|
+
if fit_results.fixednames is not None:
|
|
415
|
+
for name, val in zip(
|
|
416
|
+
fit_results.fixednames, fit_results.fixedvals
|
|
417
|
+
):
|
|
418
|
+
results_dict["fixed_variables"][name] = {"value": val}
|
|
419
|
+
# constraints
|
|
420
|
+
results_dict["constraints"] = {}
|
|
421
|
+
if fit_results.connames and fit_results.showcon:
|
|
422
|
+
for con in fit_results.conresults.values():
|
|
423
|
+
for i, loc in enumerate(con.conlocs):
|
|
424
|
+
names = [obj.name for obj in loc]
|
|
425
|
+
name = ".".join(names)
|
|
426
|
+
val = con.convals[i]
|
|
427
|
+
unc = con.conuncs[i]
|
|
428
|
+
results_dict["constraints"][name] = {
|
|
429
|
+
"value": val,
|
|
430
|
+
"uncertainty": unc,
|
|
431
|
+
}
|
|
432
|
+
# covariance matrix
|
|
433
|
+
results_dict["covariance_matrix"] = fit_results.cov.tolist()
|
|
434
|
+
# certainty
|
|
435
|
+
certain = True
|
|
436
|
+
for con in fit_results.conresults.values():
|
|
437
|
+
if (con.dy == 1).all():
|
|
438
|
+
certain = False
|
|
439
|
+
results_dict["certain"] = certain
|
|
440
|
+
if filename is not None:
|
|
441
|
+
with open(filename, "w") as f:
|
|
442
|
+
json.dump(results_dict, f, indent=2)
|
|
443
|
+
return results_dict
|
|
444
|
+
|
|
445
|
+
else:
|
|
446
|
+
raise ValueError(
|
|
447
|
+
f"Unsupported mode: {mode}. Please use 'json' or 'txt'."
|
|
448
|
+
)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
|
|
3
|
+
from pdfbl.sequential import __version__
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main():
|
|
7
|
+
"""Entry point for the pdfbl-cli.
|
|
8
|
+
|
|
9
|
+
Examples
|
|
10
|
+
--------
|
|
11
|
+
>>> pdfbl-cli --version
|
|
12
|
+
"""
|
|
13
|
+
parser = argparse.ArgumentParser(
|
|
14
|
+
description=(
|
|
15
|
+
"Scripts for running sequential PDF refinements "
|
|
16
|
+
"using diffpy.cmi automatically"
|
|
17
|
+
)
|
|
18
|
+
)
|
|
19
|
+
parser.add_argument(
|
|
20
|
+
"--version",
|
|
21
|
+
action="version",
|
|
22
|
+
version=f"pdfbl.sequential {__version__}",
|
|
23
|
+
help="Show the version of pdfbl.sequential and exit.",
|
|
24
|
+
)
|
|
25
|
+
parser.parse_args()
|
|
@@ -0,0 +1,557 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import re
|
|
3
|
+
import threading
|
|
4
|
+
import time
|
|
5
|
+
import warnings
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from queue import Queue
|
|
8
|
+
from types import SimpleNamespace
|
|
9
|
+
from typing import Literal
|
|
10
|
+
|
|
11
|
+
from bg_mpl_stylesheets.styles import all_styles
|
|
12
|
+
from diffpy.srfit.fitbase import FitResults
|
|
13
|
+
from matplotlib import pyplot as plt
|
|
14
|
+
from prompt_toolkit import PromptSession
|
|
15
|
+
from prompt_toolkit.patch_stdout import patch_stdout
|
|
16
|
+
|
|
17
|
+
from pdfbl.sequential.pdfadapter import PDFAdapter
|
|
18
|
+
|
|
19
|
+
plt.style.use(all_styles["bg-style"])
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class SequentialCMIRunner:
|
|
23
|
+
def __init__(self):
|
|
24
|
+
self.input_files_known = []
|
|
25
|
+
self.input_files_completed = []
|
|
26
|
+
self.input_files_running = []
|
|
27
|
+
self.adapter = PDFAdapter()
|
|
28
|
+
self.visualization_data = {}
|
|
29
|
+
|
|
30
|
+
def _validate_inputs(self):
|
|
31
|
+
for path_name in [
|
|
32
|
+
"input_data_dir",
|
|
33
|
+
"output_result_dir",
|
|
34
|
+
]:
|
|
35
|
+
if not Path(self.inputs[path_name]).exists():
|
|
36
|
+
raise FileNotFoundError(
|
|
37
|
+
f"Path '{self.inputs[path_name]}' for "
|
|
38
|
+
f"'{path_name}' does not exist. Please check the "
|
|
39
|
+
"provided path."
|
|
40
|
+
)
|
|
41
|
+
if not Path(self.inputs[path_name]).is_dir():
|
|
42
|
+
raise NotADirectoryError(
|
|
43
|
+
f"Path '{self.inputs[path_name]}' for "
|
|
44
|
+
f"'{path_name}' is not a directory. Please check the "
|
|
45
|
+
"provided path."
|
|
46
|
+
)
|
|
47
|
+
if not Path(self.inputs["structure_path"]).exists():
|
|
48
|
+
raise FileNotFoundError(
|
|
49
|
+
f"Structure file '{self.inputs['structure_path']}' does not "
|
|
50
|
+
"exist. Please check the provided path."
|
|
51
|
+
)
|
|
52
|
+
profile_files = list(Path(self.inputs["input_data_dir"]).glob("*"))
|
|
53
|
+
if len(profile_files) > 0: # skip variable checking if no input files
|
|
54
|
+
for tmp_file_path in profile_files:
|
|
55
|
+
matches = re.findall(
|
|
56
|
+
self.inputs["filename_order_pattern"], tmp_file_path.name
|
|
57
|
+
)
|
|
58
|
+
if len(matches) == 0:
|
|
59
|
+
raise ValueError(
|
|
60
|
+
f"Input file '{tmp_file_path}' does not match the "
|
|
61
|
+
"filename order pattern. Please check the pattern "
|
|
62
|
+
"or the input files."
|
|
63
|
+
)
|
|
64
|
+
tmp_adatper = PDFAdapter()
|
|
65
|
+
tmp_adatper.initialize_profile(str(tmp_file_path))
|
|
66
|
+
tmp_adatper.initialize_structures([self.inputs["structure_path"]])
|
|
67
|
+
tmp_adatper.initialize_contribution()
|
|
68
|
+
tmp_adatper.initialize_recipe()
|
|
69
|
+
allowed_variable_names = list(
|
|
70
|
+
tmp_adatper.recipe._parameters.keys()
|
|
71
|
+
)
|
|
72
|
+
for var_name in self.inputs["refinable_variable_names"]:
|
|
73
|
+
if var_name not in allowed_variable_names:
|
|
74
|
+
raise ValueError(
|
|
75
|
+
f"Refinable variable '{var_name}' not found in the "
|
|
76
|
+
"recipe. Please choose from the existing variables: "
|
|
77
|
+
f"{allowed_variable_names}"
|
|
78
|
+
)
|
|
79
|
+
for var_name in self.inputs.get("plot_variable_names", []):
|
|
80
|
+
if var_name not in allowed_variable_names:
|
|
81
|
+
raise ValueError(
|
|
82
|
+
f"Variable '{var_name}' is not found in the recipe. "
|
|
83
|
+
"Please choose from the existing variables: "
|
|
84
|
+
f"{allowed_variable_names}"
|
|
85
|
+
)
|
|
86
|
+
else:
|
|
87
|
+
warnings.warn(
|
|
88
|
+
"No input profile files found in the input data directory. "
|
|
89
|
+
"Skipping variable name validation."
|
|
90
|
+
)
|
|
91
|
+
allowed_result_entry_names = [
|
|
92
|
+
"residual",
|
|
93
|
+
"contributions",
|
|
94
|
+
"restraints",
|
|
95
|
+
"chi2",
|
|
96
|
+
"reduced_chi2",
|
|
97
|
+
]
|
|
98
|
+
for entry_name in self.inputs.get("plot_result_names", []):
|
|
99
|
+
if entry_name not in allowed_result_entry_names:
|
|
100
|
+
raise ValueError(
|
|
101
|
+
f"Result entry '{entry_name}' is not a valid entry to "
|
|
102
|
+
"plot. Please choose from the following entries: "
|
|
103
|
+
f"{allowed_result_entry_names}"
|
|
104
|
+
)
|
|
105
|
+
for entry_name in self.inputs.get(
|
|
106
|
+
"plot_intermediate_result_names", []
|
|
107
|
+
):
|
|
108
|
+
if entry_name not in allowed_result_entry_names:
|
|
109
|
+
raise ValueError(
|
|
110
|
+
f"Intermediate result '{entry_name}' is not a valid "
|
|
111
|
+
"entry to plot. Please choose from the following "
|
|
112
|
+
"entries: "
|
|
113
|
+
f"{allowed_result_entry_names}"
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
def load_inputs(
|
|
117
|
+
self,
|
|
118
|
+
input_data_dir,
|
|
119
|
+
structure_path,
|
|
120
|
+
output_result_dir="results",
|
|
121
|
+
filename_order_pattern=r"(\d+)K\.gr",
|
|
122
|
+
whether_plot_y=False,
|
|
123
|
+
whether_plot_ycalc=False,
|
|
124
|
+
plot_variable_names=None,
|
|
125
|
+
plot_result_names=None,
|
|
126
|
+
plot_intermediate_result_names=None,
|
|
127
|
+
refinable_variable_names=None,
|
|
128
|
+
initial_variable_values=None,
|
|
129
|
+
xmin=None,
|
|
130
|
+
xmax=None,
|
|
131
|
+
dx=None,
|
|
132
|
+
qmin=None,
|
|
133
|
+
qmax=None,
|
|
134
|
+
show_plot=True,
|
|
135
|
+
):
|
|
136
|
+
"""Load and validate input configuration for sequential PDF
|
|
137
|
+
refinement.
|
|
138
|
+
|
|
139
|
+
This method initializes the sequential CMI runner with input data,
|
|
140
|
+
structure information, and refinement parameters, and the plotting
|
|
141
|
+
configuration.
|
|
142
|
+
|
|
143
|
+
Parameters
|
|
144
|
+
----------
|
|
145
|
+
input_data_dir : str
|
|
146
|
+
The path to the directory containing input PDF profile files.
|
|
147
|
+
structure_path : str
|
|
148
|
+
The path to the structure file (e.g., CIF format) used for
|
|
149
|
+
refinement.
|
|
150
|
+
output_result_dir : str
|
|
151
|
+
The path to the directory for storing refinement results.
|
|
152
|
+
Default is "results".
|
|
153
|
+
filename_order_pattern : str
|
|
154
|
+
The regular expression pattern to extract ordering information
|
|
155
|
+
from filenames.
|
|
156
|
+
Default is r"(\d+)K\.gr" to extract temperature values from
|
|
157
|
+
filenames.
|
|
158
|
+
refinable_variable_names : list of str
|
|
159
|
+
The list of variable names to refine.
|
|
160
|
+
Must exist in the recipe.
|
|
161
|
+
Default variable names are all possible variables that can
|
|
162
|
+
be created from the input structure and profile.
|
|
163
|
+
initial_variable_values : dict
|
|
164
|
+
The dictionary mapping variable names to their initial values.
|
|
165
|
+
Default is None.
|
|
166
|
+
xmin : float
|
|
167
|
+
The minimum x-value for the PDF profile.
|
|
168
|
+
Default is the value parsed from the input file.
|
|
169
|
+
xmax : float
|
|
170
|
+
The maximum x-value for the PDF profile.
|
|
171
|
+
Default is the value parsed from the input file.
|
|
172
|
+
dx : float
|
|
173
|
+
The step size for the PDF profile.
|
|
174
|
+
Default is the value parsed from the input file.
|
|
175
|
+
qmin : float
|
|
176
|
+
The minimum q-value for the PDF profile.
|
|
177
|
+
Default is the value parsed from the input file.
|
|
178
|
+
qmax : float
|
|
179
|
+
The maximum q-value for the PDF profile.
|
|
180
|
+
Default is the value parsed from the input file.
|
|
181
|
+
show_plot : bool
|
|
182
|
+
Whether to display plots during refinement. Default is True.
|
|
183
|
+
whether_plot_y : bool
|
|
184
|
+
Whether to plot the experimental PDF data (y). Default is False.
|
|
185
|
+
whether_plot_ycalc : bool
|
|
186
|
+
Whether to plot the calculated PDF data (ycalc). Default is False.
|
|
187
|
+
plot_variable_names : list of str
|
|
188
|
+
The list of variable names to plot during refinement.
|
|
189
|
+
Default is None.
|
|
190
|
+
plot_result_names : list of str
|
|
191
|
+
The list of fit result entries to plot.
|
|
192
|
+
Allowed values: "residual", "contributions", "restraints", "chi2",
|
|
193
|
+
"reduced_chi2". Default is None.
|
|
194
|
+
plot_intermediate_result_names : list of str
|
|
195
|
+
The list of intermediate result entries to plot during refinement.
|
|
196
|
+
Allowed values: "residual", "contributions", "restraints", "chi2",
|
|
197
|
+
"reduced_chi2". Default is None.
|
|
198
|
+
|
|
199
|
+
Raises
|
|
200
|
+
------
|
|
201
|
+
FileNotFoundError
|
|
202
|
+
If the input data directory, output result directory, or structure
|
|
203
|
+
file does not exist.
|
|
204
|
+
NotADirectoryError
|
|
205
|
+
If input_data_dir or output_result_dir is not a directory.
|
|
206
|
+
ValueError
|
|
207
|
+
If a refinable variable name is not found in the recipe, or if a
|
|
208
|
+
plot result name is not valid.
|
|
209
|
+
|
|
210
|
+
Examples
|
|
211
|
+
--------
|
|
212
|
+
>>> runner = SequentialCMIRunner()
|
|
213
|
+
>>> runner.load_inputs(
|
|
214
|
+
... input_data_dir="./data",
|
|
215
|
+
... structure_path="./structure.cif",
|
|
216
|
+
... output_result_dir="./results",
|
|
217
|
+
... refinable_variable_names=["a", "all"],
|
|
218
|
+
... plot_variable_names=["a"],
|
|
219
|
+
... plot_result_names=["chi2"],
|
|
220
|
+
... plot_intermediate_result_names=["residual"],
|
|
221
|
+
... )
|
|
222
|
+
""" # noqa: W605
|
|
223
|
+
self.inputs = {
|
|
224
|
+
"input_data_dir": input_data_dir,
|
|
225
|
+
"structure_path": structure_path,
|
|
226
|
+
"output_result_dir": output_result_dir,
|
|
227
|
+
"filename_order_pattern": filename_order_pattern,
|
|
228
|
+
"xmin": xmin,
|
|
229
|
+
"xmax": xmax,
|
|
230
|
+
"dx": dx,
|
|
231
|
+
"qmin": qmin,
|
|
232
|
+
"qmax": qmax,
|
|
233
|
+
"refinable_variable_names": refinable_variable_names or [],
|
|
234
|
+
"initial_variable_values": initial_variable_values or {},
|
|
235
|
+
"whether_plot_y": whether_plot_y,
|
|
236
|
+
"whether_plot_ycalc": whether_plot_ycalc,
|
|
237
|
+
"plot_variable_names": plot_variable_names or [],
|
|
238
|
+
"plot_result_names": plot_result_names or [],
|
|
239
|
+
"plot_intermediate_result_names": plot_intermediate_result_names
|
|
240
|
+
or [],
|
|
241
|
+
}
|
|
242
|
+
self.show_plot = show_plot
|
|
243
|
+
self._validate_inputs()
|
|
244
|
+
self._initialize_plots()
|
|
245
|
+
|
|
246
|
+
def _initialize_plots(self):
|
|
247
|
+
whether_plot_y = self.inputs["whether_plot_y"]
|
|
248
|
+
whether_plot_ycalc = self.inputs["whether_plot_ycalc"]
|
|
249
|
+
plot_variable_names = self.inputs["plot_variable_names"]
|
|
250
|
+
plot_result_names = self.inputs["plot_result_names"]
|
|
251
|
+
plot_intermediate_result_names = self.inputs[
|
|
252
|
+
"plot_intermediate_result_names"
|
|
253
|
+
]
|
|
254
|
+
if whether_plot_y and whether_plot_ycalc:
|
|
255
|
+
fig, _ = plt.subplots(2, 1)
|
|
256
|
+
label = ["ycalc", "y"]
|
|
257
|
+
elif whether_plot_ycalc or whether_plot_y:
|
|
258
|
+
fig, _ = plt.subplots()
|
|
259
|
+
if whether_plot_ycalc:
|
|
260
|
+
label = ["ycalc"]
|
|
261
|
+
else:
|
|
262
|
+
label = ["y"]
|
|
263
|
+
else:
|
|
264
|
+
fig = None
|
|
265
|
+
if fig:
|
|
266
|
+
axes = fig.axes
|
|
267
|
+
lines = []
|
|
268
|
+
for i in range(len(axes)):
|
|
269
|
+
(line,) = axes[i].plot(
|
|
270
|
+
[],
|
|
271
|
+
[],
|
|
272
|
+
label=label[i],
|
|
273
|
+
color=plt.rcParams["axes.prop_cycle"].by_key()["color"][i],
|
|
274
|
+
)
|
|
275
|
+
lines.append(line)
|
|
276
|
+
self.visualization_data[label[i]] = {
|
|
277
|
+
"line": line,
|
|
278
|
+
"xdata": Queue(),
|
|
279
|
+
"ydata": Queue(),
|
|
280
|
+
}
|
|
281
|
+
fig.legend()
|
|
282
|
+
names = ["variables", "results", "intermediate_results"]
|
|
283
|
+
plot_tasks = [
|
|
284
|
+
plot_variable_names,
|
|
285
|
+
plot_result_names,
|
|
286
|
+
plot_intermediate_result_names,
|
|
287
|
+
]
|
|
288
|
+
for i in range(len(plot_tasks)):
|
|
289
|
+
if plot_tasks[i] is not None:
|
|
290
|
+
self.visualization_data[names[i]] = {}
|
|
291
|
+
for var_name in plot_tasks[i]:
|
|
292
|
+
fig, ax = plt.subplots()
|
|
293
|
+
(line,) = ax.plot([], [], label=var_name, marker="o")
|
|
294
|
+
self.visualization_data[names[i]][var_name] = {
|
|
295
|
+
"line": line,
|
|
296
|
+
"buffer": [],
|
|
297
|
+
"ydata": Queue(),
|
|
298
|
+
}
|
|
299
|
+
fig.suptitle(f"{names[i].capitalize()}: {var_name}")
|
|
300
|
+
if plot_intermediate_result_names is not None:
|
|
301
|
+
for var_name in plot_intermediate_result_names:
|
|
302
|
+
self.adapter.monitor_intermediate_results(
|
|
303
|
+
var_name,
|
|
304
|
+
step=10,
|
|
305
|
+
queue=self.visualization_data["intermediate_results"][
|
|
306
|
+
var_name
|
|
307
|
+
]["ydata"],
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
def _update_plot(self):
|
|
311
|
+
for key, plot_pack in self.visualization_data.items():
|
|
312
|
+
if key in ["ycalc", "y"]:
|
|
313
|
+
if not plot_pack["xdata"].empty():
|
|
314
|
+
line = plot_pack["line"]
|
|
315
|
+
xdata = plot_pack["xdata"].get()
|
|
316
|
+
ydata = plot_pack["ydata"].get()
|
|
317
|
+
line.set_xdata(xdata)
|
|
318
|
+
line.set_ydata(ydata)
|
|
319
|
+
line.axes.relim()
|
|
320
|
+
line.axes.autoscale_view()
|
|
321
|
+
elif (
|
|
322
|
+
key == "variables"
|
|
323
|
+
or key == "results"
|
|
324
|
+
or key == "intermediate_results"
|
|
325
|
+
):
|
|
326
|
+
for _, data_pack in plot_pack.items():
|
|
327
|
+
if not data_pack["ydata"].empty():
|
|
328
|
+
line = data_pack["line"]
|
|
329
|
+
buffer = data_pack["buffer"]
|
|
330
|
+
new_y = data_pack["ydata"].get()
|
|
331
|
+
buffer.append(new_y)
|
|
332
|
+
xdata = list(range(1, len(buffer) + 1))
|
|
333
|
+
ydata = buffer
|
|
334
|
+
line.set_xdata(xdata)
|
|
335
|
+
line.set_ydata(ydata)
|
|
336
|
+
line.axes.relim()
|
|
337
|
+
line.axes.autoscale_view()
|
|
338
|
+
|
|
339
|
+
def _check_for_new_data(self):
|
|
340
|
+
input_data_dir = self.inputs["input_data_dir"]
|
|
341
|
+
filename_order_pattern = self.inputs["filename_order_pattern"]
|
|
342
|
+
files = [file for file in Path(input_data_dir).glob("*")]
|
|
343
|
+
sorted_file = sorted(
|
|
344
|
+
files,
|
|
345
|
+
key=lambda file: int(
|
|
346
|
+
re.findall(filename_order_pattern, file.name)[0]
|
|
347
|
+
),
|
|
348
|
+
)
|
|
349
|
+
if (
|
|
350
|
+
self.input_files_known
|
|
351
|
+
!= sorted_file[: len(self.input_files_known)]
|
|
352
|
+
):
|
|
353
|
+
raise RuntimeError(
|
|
354
|
+
"Wrong order to run sequential toolset is detected. "
|
|
355
|
+
"This is likely due to files appearing in the input directory "
|
|
356
|
+
"in the wrong order. Please restart the sequential toolset."
|
|
357
|
+
)
|
|
358
|
+
if self.input_files_known == sorted_file:
|
|
359
|
+
return
|
|
360
|
+
self.input_files_known = sorted_file
|
|
361
|
+
self.input_files_running = [
|
|
362
|
+
f
|
|
363
|
+
for f in self.input_files_known
|
|
364
|
+
if f not in self.input_files_completed
|
|
365
|
+
]
|
|
366
|
+
print(f"{[str(f) for f in self.input_files_running]} detected.")
|
|
367
|
+
|
|
368
|
+
def set_start_input_file(
|
|
369
|
+
self, input_filename, input_filename_to_result_filename
|
|
370
|
+
):
|
|
371
|
+
"""Set the starting input file for sequential refinement and
|
|
372
|
+
continue the interrupted sequential refinement from that point.
|
|
373
|
+
|
|
374
|
+
Parameters
|
|
375
|
+
----------
|
|
376
|
+
input_filename : str
|
|
377
|
+
The name of the input file to start from. This file must be in the
|
|
378
|
+
input data directory.
|
|
379
|
+
input_filename_to_result_filename : function
|
|
380
|
+
The function that takes an input filename and returns the
|
|
381
|
+
corresponding result filename. This is used to locate the last
|
|
382
|
+
result file for loading variable values.
|
|
383
|
+
"""
|
|
384
|
+
self._check_for_new_data()
|
|
385
|
+
input_file_path = Path(self.inputs["input_data_dir"]) / input_filename
|
|
386
|
+
if input_file_path not in self.input_files_known:
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f"Input file {input_filename} not found in known input files."
|
|
389
|
+
)
|
|
390
|
+
start_index = self.input_files_known.index(input_file_path)
|
|
391
|
+
self.input_files_completed = self.input_files_known[:start_index]
|
|
392
|
+
self.input_files_running = self.input_files_known[start_index:]
|
|
393
|
+
last_result_file = input_filename_to_result_filename(
|
|
394
|
+
self.input_files_completed[-1].name
|
|
395
|
+
)
|
|
396
|
+
last_result_file = (
|
|
397
|
+
Path(self.inputs["output_result_dir"]) / last_result_file
|
|
398
|
+
)
|
|
399
|
+
if not Path(last_result_file).exists():
|
|
400
|
+
raise FileNotFoundError(
|
|
401
|
+
f"Result file {last_result_file} not found. "
|
|
402
|
+
"Cannot load last result variable values. "
|
|
403
|
+
"Please check the provided function or use "
|
|
404
|
+
"an earlier input file."
|
|
405
|
+
)
|
|
406
|
+
last_result_variables_values = json.load(open(last_result_file, "r"))[
|
|
407
|
+
"variables"
|
|
408
|
+
]
|
|
409
|
+
last_result_variables_values = {
|
|
410
|
+
name: pack["value"]
|
|
411
|
+
for name, pack in last_result_variables_values.items()
|
|
412
|
+
}
|
|
413
|
+
self.last_result_variables_values = last_result_variables_values
|
|
414
|
+
print(f"Starting from input file: {self.input_files_running[0].name}")
|
|
415
|
+
|
|
416
|
+
def _run_one_cycle(self, stop_event=SimpleNamespace(is_set=lambda: False)):
|
|
417
|
+
self._check_for_new_data()
|
|
418
|
+
xmin = self.inputs["xmin"]
|
|
419
|
+
xmax = self.inputs["xmax"]
|
|
420
|
+
dx = self.inputs["dx"]
|
|
421
|
+
qmin = self.inputs["qmin"]
|
|
422
|
+
qmax = self.inputs["qmax"]
|
|
423
|
+
structure_path = self.inputs["structure_path"]
|
|
424
|
+
output_result_dir = self.inputs["output_result_dir"]
|
|
425
|
+
initial_variable_values = self.inputs["initial_variable_values"]
|
|
426
|
+
refinable_variable_names = self.inputs["refinable_variable_names"]
|
|
427
|
+
if not self.input_files_running:
|
|
428
|
+
return None
|
|
429
|
+
for input_file in self.input_files_running:
|
|
430
|
+
if stop_event.is_set():
|
|
431
|
+
break
|
|
432
|
+
print(f"Processing {input_file.name}...")
|
|
433
|
+
self.adapter.initialize_profile(
|
|
434
|
+
str(input_file),
|
|
435
|
+
xmin=xmin,
|
|
436
|
+
xmax=xmax,
|
|
437
|
+
dx=dx,
|
|
438
|
+
qmin=qmin,
|
|
439
|
+
qmax=qmax,
|
|
440
|
+
)
|
|
441
|
+
self.adapter.initialize_structures([structure_path])
|
|
442
|
+
self.adapter.initialize_contribution()
|
|
443
|
+
self.adapter.initialize_recipe()
|
|
444
|
+
if not hasattr(self, "last_result_variables_values"):
|
|
445
|
+
self.last_result_variables_values = initial_variable_values
|
|
446
|
+
self.adapter.set_initial_variable_values(
|
|
447
|
+
self.last_result_variables_values
|
|
448
|
+
)
|
|
449
|
+
if refinable_variable_names is None:
|
|
450
|
+
refinable_variable_names = list(initial_variable_values.keys())
|
|
451
|
+
self.adapter.refine_variables(refinable_variable_names)
|
|
452
|
+
results = self.adapter.save_results(
|
|
453
|
+
filename=str(
|
|
454
|
+
Path(output_result_dir) / f"{input_file.stem}_result.json"
|
|
455
|
+
),
|
|
456
|
+
mode="dict",
|
|
457
|
+
)
|
|
458
|
+
self.last_result_variables_values = {
|
|
459
|
+
name: pack["value"]
|
|
460
|
+
for name, pack in results["variables"].items()
|
|
461
|
+
}
|
|
462
|
+
self.input_files_completed.append(input_file)
|
|
463
|
+
if "ycalc" in self.visualization_data:
|
|
464
|
+
xdata = self.adapter.recipe.pdfcontribution.profile.x
|
|
465
|
+
ydata = self.adapter.recipe.pdfcontribution.profile.ycalc
|
|
466
|
+
self.visualization_data["ycalc"]["xdata"].put(xdata)
|
|
467
|
+
self.visualization_data["ycalc"]["ydata"].put(ydata)
|
|
468
|
+
if "y" in self.visualization_data:
|
|
469
|
+
xdata = self.adapter.recipe.pdfcontribution.profile.x
|
|
470
|
+
ydata = self.adapter.recipe.pdfcontribution.profile.y
|
|
471
|
+
self.visualization_data["y"]["xdata"].put(xdata)
|
|
472
|
+
self.visualization_data["y"]["ydata"].put(ydata)
|
|
473
|
+
for var_name in self.visualization_data.get("variables", {}):
|
|
474
|
+
new_value = self.adapter.recipe._parameters[var_name].value
|
|
475
|
+
self.visualization_data["variables"][var_name]["ydata"].put(
|
|
476
|
+
new_value
|
|
477
|
+
)
|
|
478
|
+
for entry_name in self.visualization_data.get("results", {}):
|
|
479
|
+
fit_results = FitResults(self.adapter.recipe)
|
|
480
|
+
entry_value = getattr(fit_results, entry_name)
|
|
481
|
+
self.visualization_data["results"][entry_name]["ydata"].put(
|
|
482
|
+
entry_value
|
|
483
|
+
)
|
|
484
|
+
print("Completed!")
|
|
485
|
+
self.input_files_running = []
|
|
486
|
+
|
|
487
|
+
def run(self, mode: Literal["batch", "stream"]):
|
|
488
|
+
"""Run the sequential refinement process in either batch or
|
|
489
|
+
streaming mode.
|
|
490
|
+
|
|
491
|
+
Parameters
|
|
492
|
+
----------
|
|
493
|
+
mode : str
|
|
494
|
+
The mode to run the sequential refinement. Must be either "batch"
|
|
495
|
+
or "stream". In "batch" mode, the toolset will run through all
|
|
496
|
+
available input files once and then stop. In "stream" mode, the
|
|
497
|
+
runner will continuously monitor the input data directory for new
|
|
498
|
+
files and process them as they appear, until the user decides
|
|
499
|
+
to stop the process.
|
|
500
|
+
"""
|
|
501
|
+
if mode == "batch":
|
|
502
|
+
self._run_one_cycle()
|
|
503
|
+
self._update_plot()
|
|
504
|
+
elif mode == "stream":
|
|
505
|
+
stop_event = threading.Event()
|
|
506
|
+
session = PromptSession()
|
|
507
|
+
if (self.visualization_data is not None) and self.show_plot:
|
|
508
|
+
plt.ion()
|
|
509
|
+
plt.pause(0.01)
|
|
510
|
+
|
|
511
|
+
def stream_loop():
|
|
512
|
+
while not stop_event.is_set():
|
|
513
|
+
self._run_one_cycle(stop_event)
|
|
514
|
+
stop_event.wait(1) # Check for new data every 1s
|
|
515
|
+
|
|
516
|
+
def input_loop():
|
|
517
|
+
with patch_stdout():
|
|
518
|
+
print("=== COMMANDS ===")
|
|
519
|
+
print("Type STOP to exit")
|
|
520
|
+
print("================")
|
|
521
|
+
while not stop_event.is_set():
|
|
522
|
+
cmd = session.prompt("> ")
|
|
523
|
+
if cmd.strip() == "STOP":
|
|
524
|
+
stop_event.set()
|
|
525
|
+
print(
|
|
526
|
+
"Stopping the streaming sequential toolset..."
|
|
527
|
+
)
|
|
528
|
+
else:
|
|
529
|
+
print(
|
|
530
|
+
"Unrecognized input. "
|
|
531
|
+
"Please type 'STOP' to end."
|
|
532
|
+
)
|
|
533
|
+
visualization_data = {}
|
|
534
|
+
for (
|
|
535
|
+
category_name,
|
|
536
|
+
data_pack,
|
|
537
|
+
) in self.visualization_data.items():
|
|
538
|
+
for var_name, var_pack in data_pack.items():
|
|
539
|
+
if "buffer" in var_pack:
|
|
540
|
+
visualization_data[category_name] = {
|
|
541
|
+
var_name: var_pack["buffer"]
|
|
542
|
+
}
|
|
543
|
+
with open("visualization_data.json", "w") as f:
|
|
544
|
+
json.dump(visualization_data, f, indent=2)
|
|
545
|
+
|
|
546
|
+
input_thread = threading.Thread(target=input_loop)
|
|
547
|
+
input_thread.start()
|
|
548
|
+
fit_thread = threading.Thread(target=stream_loop)
|
|
549
|
+
fit_thread.start()
|
|
550
|
+
while not stop_event.is_set():
|
|
551
|
+
self._update_plot()
|
|
552
|
+
plt.pause(0.01)
|
|
553
|
+
time.sleep(1)
|
|
554
|
+
fit_thread.join()
|
|
555
|
+
input_thread.join()
|
|
556
|
+
else:
|
|
557
|
+
raise ValueError(f"Unknown mode: {mode}")
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
#!/usr/bin/env python
|
|
2
|
+
##############################################################################
|
|
3
|
+
#
|
|
4
|
+
# (c) 2025 Simon Billinge.
|
|
5
|
+
# All rights reserved.
|
|
6
|
+
#
|
|
7
|
+
# File coded by: members of the Billinge Group and PDF beamline at NSLS-II.
|
|
8
|
+
#
|
|
9
|
+
# See GitHub contributions for a more detailed list of contributors.
|
|
10
|
+
# https://github.com/pdf-bl/pdfbl.sequential/graphs/contributors # noqa: E501
|
|
11
|
+
#
|
|
12
|
+
# See LICENSE.rst for license information.
|
|
13
|
+
#
|
|
14
|
+
##############################################################################
|
|
15
|
+
"""Definition of __version__."""
|
|
16
|
+
|
|
17
|
+
# We do not use the other three variables, but can be added back if needed.
|
|
18
|
+
# __all__ = ["__date__", "__git_commit__", "__timestamp__", "__version__"]
|
|
19
|
+
|
|
20
|
+
# obtain version information
|
|
21
|
+
from importlib.metadata import PackageNotFoundError, version
|
|
22
|
+
|
|
23
|
+
try:
|
|
24
|
+
__version__ = version("pdfbl.sequential")
|
|
25
|
+
except PackageNotFoundError:
|
|
26
|
+
__version__ = "unknown"
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pdfbl.sequential
|
|
3
|
+
Version: 0.1.0rc0
|
|
4
|
+
Summary: Automated sequential refinements of PDF data
|
|
5
|
+
Author-email: Simon Billinge <sb2896@columbia.edu>
|
|
6
|
+
Maintainer-email: Simon Billinge <sb2896@columbia.edu>
|
|
7
|
+
Project-URL: Homepage, https://github.com/pdf-bl/pdfbl.sequential/
|
|
8
|
+
Project-URL: Issues, https://github.com/pdf-bl/pdfbl.sequential/issues/
|
|
9
|
+
Keywords: diffraction,PDF,X-ray,neutron,nsls-ii
|
|
10
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
11
|
+
Classifier: Environment :: Console
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Intended Audience :: Science/Research
|
|
14
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
15
|
+
Classifier: Operating System :: MacOS :: MacOS X
|
|
16
|
+
Classifier: Operating System :: Microsoft :: Windows
|
|
17
|
+
Classifier: Operating System :: POSIX
|
|
18
|
+
Classifier: Operating System :: Unix
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering :: Chemistry
|
|
23
|
+
Requires-Python: <3.15,>=3.12
|
|
24
|
+
Description-Content-Type: text/x-rst
|
|
25
|
+
License-File: LICENSE.rst
|
|
26
|
+
License-File: AUTHORS.rst
|
|
27
|
+
Requires-Dist: numpy
|
|
28
|
+
Requires-Dist: diffpy.cmi
|
|
29
|
+
Dynamic: license-file
|
|
30
|
+
|
|
31
|
+
|Icon| |title|_
|
|
32
|
+
===============
|
|
33
|
+
|
|
34
|
+
.. |title| replace:: pdfbl.sequential
|
|
35
|
+
.. _title: https://pdf-bl.github.io/pdfbl.sequential
|
|
36
|
+
|
|
37
|
+
.. |Icon| image:: https://avatars.githubusercontent.com/pdf-bl
|
|
38
|
+
:target: https://pdf-bl.github.io/pdfbl.sequential
|
|
39
|
+
:height: 100px
|
|
40
|
+
|
|
41
|
+
|PyPI| |Forge| |PythonVersion| |PR|
|
|
42
|
+
|
|
43
|
+
|CI| |Codecov| |Black| |Tracking|
|
|
44
|
+
|
|
45
|
+
.. |Black| image:: https://img.shields.io/badge/code_style-black-black
|
|
46
|
+
:target: https://github.com/psf/black
|
|
47
|
+
|
|
48
|
+
.. |CI| image:: https://github.com/pdf-bl/pdfbl.sequential/actions/workflows/matrix-and-codecov-on-merge-to-main.yml/badge.svg
|
|
49
|
+
:target: https://github.com/pdf-bl/pdfbl.sequential/actions/workflows/matrix-and-codecov-on-merge-to-main.yml
|
|
50
|
+
|
|
51
|
+
.. |Codecov| image:: https://codecov.io/gh/pdf-bl/pdfbl.sequential/branch/main/graph/badge.svg
|
|
52
|
+
:target: https://codecov.io/gh/pdf-bl/pdfbl.sequential
|
|
53
|
+
|
|
54
|
+
.. |Forge| image:: https://img.shields.io/conda/vn/conda-forge/pdfbl.sequential
|
|
55
|
+
:target: https://anaconda.org/conda-forge/pdfbl.sequential
|
|
56
|
+
|
|
57
|
+
.. |PR| image:: https://img.shields.io/badge/PR-Welcome-29ab47ff
|
|
58
|
+
:target: https://github.com/pdf-bl/pdfbl.sequential/pulls
|
|
59
|
+
|
|
60
|
+
.. |PyPI| image:: https://img.shields.io/pypi/v/pdfbl.sequential
|
|
61
|
+
:target: https://pypi.org/project/pdfbl.sequential/
|
|
62
|
+
|
|
63
|
+
.. |PythonVersion| image:: https://img.shields.io/pypi/pyversions/pdfbl.sequential
|
|
64
|
+
:target: https://pypi.org/project/pdfbl.sequential/
|
|
65
|
+
|
|
66
|
+
.. |Tracking| image:: https://img.shields.io/badge/issue_tracking-github-blue
|
|
67
|
+
:target: https://github.com/pdf-bl/pdfbl.sequential/issues
|
|
68
|
+
|
|
69
|
+
Automated sequential refinements of PDF data
|
|
70
|
+
|
|
71
|
+
Scripts for running sequential PDF refinements using diffpy.cmi automatically
|
|
72
|
+
|
|
73
|
+
For more information about the pdfbl.sequential library, please consult our `online documentation <https://pdf-bl.github.io/pdfbl.sequential>`_.
|
|
74
|
+
|
|
75
|
+
Citation
|
|
76
|
+
--------
|
|
77
|
+
|
|
78
|
+
If you use pdfbl.sequential in a scientific publication, we would like you to cite this package as
|
|
79
|
+
|
|
80
|
+
pdfbl.sequential Package, https://github.com/pdf-bl/pdfbl.sequential
|
|
81
|
+
|
|
82
|
+
Installation
|
|
83
|
+
------------
|
|
84
|
+
|
|
85
|
+
The preferred method is to use `Miniconda Python
|
|
86
|
+
<https://docs.conda.io/projects/miniconda/en/latest/miniconda-install.html>`_
|
|
87
|
+
and install from the "conda-forge" channel of Conda packages.
|
|
88
|
+
|
|
89
|
+
To add "conda-forge" to the conda channels, run the following in a terminal. ::
|
|
90
|
+
|
|
91
|
+
conda config --add channels conda-forge
|
|
92
|
+
|
|
93
|
+
We want to install our packages in a suitable conda environment.
|
|
94
|
+
The following creates and activates a new environment named ``pdfbl.sequential_env`` ::
|
|
95
|
+
|
|
96
|
+
conda create -n pdfbl.sequential_env pdfbl.sequential
|
|
97
|
+
conda activate pdfbl.sequential_env
|
|
98
|
+
|
|
99
|
+
The output should print the latest version displayed on the badges above.
|
|
100
|
+
|
|
101
|
+
If the above does not work, you can use ``pip`` to download and install the latest release from
|
|
102
|
+
`Python Package Index <https://pypi.python.org>`_.
|
|
103
|
+
To install using ``pip`` into your ``pdfbl.sequential_env`` environment, type ::
|
|
104
|
+
|
|
105
|
+
pip install pdfbl.sequential
|
|
106
|
+
|
|
107
|
+
If you prefer to install from sources, after installing the dependencies, obtain the source archive from
|
|
108
|
+
`GitHub <https://github.com/pdf-bl/pdfbl.sequential/>`_. Once installed, ``cd`` into your ``pdfbl.sequential`` directory
|
|
109
|
+
and run the following ::
|
|
110
|
+
|
|
111
|
+
pip install .
|
|
112
|
+
|
|
113
|
+
This package also provides command-line utilities. To check the software has been installed correctly, type ::
|
|
114
|
+
|
|
115
|
+
pdfbl.sequential --version
|
|
116
|
+
|
|
117
|
+
You can also type the following command to verify the installation. ::
|
|
118
|
+
|
|
119
|
+
python -c "import pdfbl.sequential; print(pdfbl.sequential.__version__)"
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
To view the basic usage and available commands, type ::
|
|
123
|
+
|
|
124
|
+
pdfbl.sequential -h
|
|
125
|
+
|
|
126
|
+
Examples
|
|
127
|
+
--------
|
|
128
|
+
|
|
129
|
+
To run a temperature sequential refinement, ::
|
|
130
|
+
|
|
131
|
+
from pdfbl.sequential.sequential_cmi_runner import SequentialCMIRunner
|
|
132
|
+
runner = SequentialCMIRunner()
|
|
133
|
+
runner.load_inputs(
|
|
134
|
+
input_data_dir="path/to/inputs",
|
|
135
|
+
output_result_dir="path/to/outputs",
|
|
136
|
+
structure_path="path/to/structure.cif",
|
|
137
|
+
filename_order_pattern=r"(\d+)K\.gr", # regex pattern to extract the temperature from the filename
|
|
138
|
+
)
|
|
139
|
+
runner.run(mode="batch") # or mode="stream" for running sequentially as data becomes available
|
|
140
|
+
|
|
141
|
+
Getting Started
|
|
142
|
+
---------------
|
|
143
|
+
|
|
144
|
+
You may consult our `online documentation <https://pdf-bl.github.io/pdfbl.sequential>`_ for tutorials and API references.
|
|
145
|
+
|
|
146
|
+
Support and Contribute
|
|
147
|
+
----------------------
|
|
148
|
+
|
|
149
|
+
If you see a bug or want to request a feature, please `report it as an issue <https://github.com/pdf-bl/pdfbl.sequential/issues>`_ and/or `submit a fix as a PR <https://github.com/pdf-bl/pdfbl.sequential/pulls>`_.
|
|
150
|
+
|
|
151
|
+
Feel free to fork the project and contribute. To install pdfbl.sequential
|
|
152
|
+
in a development mode, with its sources being directly used by Python
|
|
153
|
+
rather than copied to a package directory, use the following in the root
|
|
154
|
+
directory ::
|
|
155
|
+
|
|
156
|
+
pip install -e .
|
|
157
|
+
|
|
158
|
+
To ensure code quality and to prevent accidental commits into the default branch, please set up the use of our pre-commit
|
|
159
|
+
hooks.
|
|
160
|
+
|
|
161
|
+
1. Install pre-commit in your working environment by running ``conda install pre-commit``.
|
|
162
|
+
|
|
163
|
+
2. Initialize pre-commit (one time only) ``pre-commit install``.
|
|
164
|
+
|
|
165
|
+
Thereafter your code will be linted by black and isort and checked against flake8 before you can commit.
|
|
166
|
+
If it fails by black or isort, just rerun and it should pass (black and isort will modify the files so should
|
|
167
|
+
pass after they are modified). If the flake8 test fails please see the error messages and fix them manually before
|
|
168
|
+
trying to commit again.
|
|
169
|
+
|
|
170
|
+
Improvements and fixes are always appreciated.
|
|
171
|
+
|
|
172
|
+
Before contributing, please read our `Code of Conduct <https://github.com/pdf-bl/pdfbl.sequential/blob/main/CODE-OF-CONDUCT.rst>`_.
|
|
173
|
+
|
|
174
|
+
Contact
|
|
175
|
+
-------
|
|
176
|
+
|
|
177
|
+
For more information on pdfbl.sequential please visit the project `web-page <https://pdf-bl.github.io/>`_ or email Simon Billinge at sb2896@columbia.edu.
|
|
178
|
+
|
|
179
|
+
Acknowledgements
|
|
180
|
+
----------------
|
|
181
|
+
|
|
182
|
+
``pdfbl.sequential`` is built and maintained with `scikit-package <https://scikit-package.github.io/scikit-package/>`_.
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
pdfbl/__init__.py,sha256=OQGLzzFXqkhKIP-uxtsAEhZw30U9Eq14fnMwnCUjA3M,530
|
|
2
|
+
pdfbl/sequential/__init__.py,sha256=NV4nr2fA5UsHHjotDZ3pOWZ39ItmQzk-mO6RdBs4gqQ,702
|
|
3
|
+
pdfbl/sequential/pdfadapter.py,sha256=SqKvBKNJuJC5fHw2ATBfNEn0paSpRqI1wii2IWidNhc,16716
|
|
4
|
+
pdfbl/sequential/pdfbl_sequential_app.py,sha256=6KDYfBAjbRky_9ooigGJTbGOfgDRmBN5pyoexaTxATU,572
|
|
5
|
+
pdfbl/sequential/sequential_cmi_runner.py,sha256=SixfeEvUwuZBA0oZ0P27bXZ66adzPOIi-dU8X9SSHGc,23405
|
|
6
|
+
pdfbl/sequential/version.py,sha256=2rmXHJH9IzYhXpzq9dCsYzIHOT1oC4U10Wd00mmj-wg,894
|
|
7
|
+
pdfbl_sequential-0.1.0rc0.dist-info/licenses/AUTHORS.rst,sha256=2jH0ypi3P2ZuJgv9KZEblYiQTPHyu9cT_jVkYj9ip4I,200
|
|
8
|
+
pdfbl_sequential-0.1.0rc0.dist-info/licenses/LICENSE.rst,sha256=r0DOaoezNuZvD3yiowak4UhhTqTQDxeYhAzt6gOG3jM,1523
|
|
9
|
+
pdfbl_sequential-0.1.0rc0.dist-info/METADATA,sha256=sbJuY4Ir7LVTVhWhet_QM4HX8r9oymQJe_AmW9mtYKY,7298
|
|
10
|
+
pdfbl_sequential-0.1.0rc0.dist-info/WHEEL,sha256=YCfwYGOYMi5Jhw2fU4yNgwErybb2IX5PEwBKV4ZbdBo,91
|
|
11
|
+
pdfbl_sequential-0.1.0rc0.dist-info/entry_points.txt,sha256=EwyheFjtkSEpCt-ndET1mpPzMjwp7p-KcYruJYQWB_0,80
|
|
12
|
+
pdfbl_sequential-0.1.0rc0.dist-info/top_level.txt,sha256=rhhQf1P9sizprwJb29Jj8FH2WUgYMxp4viI8aIGRBh0,6
|
|
13
|
+
pdfbl_sequential-0.1.0rc0.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025, Simon Billinge.
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without
|
|
7
|
+
modification, are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
13
|
+
this list of conditions and the following disclaimer in the documentation
|
|
14
|
+
and/or other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
3. Neither the name of the copyright holder nor the names of its contributors
|
|
17
|
+
may be used to endorse or promote products derived from this software
|
|
18
|
+
without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
21
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
22
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
23
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
24
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
25
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
26
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
27
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
28
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
29
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
pdfbl
|