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 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,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.0)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ pdfbl-sequential = pdfbl.sequential.pdfbl_sequential_app:main
@@ -0,0 +1,10 @@
1
+ Authors
2
+ =======
3
+
4
+ members of the Billinge Group and PDF beamline at NSLS-II
5
+
6
+ Contributors
7
+ ------------
8
+
9
+ For a list of contributors, visit
10
+ https://github.com/pdf-bl/pdfbl.sequential/graphs/contributors
@@ -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