diffpy.apps 0.1.0__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.
diffpy/__init__.py ADDED
@@ -0,0 +1,14 @@
1
+ #!/usr/bin/env python
2
+ ##############################################################################
3
+ #
4
+ # (c) 2026 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/diffpy/diffpy.apps/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) 2026 diffpy.apps contributors.
5
+ # All rights reserved.
6
+ #
7
+ # File coded by: Yuchen Xiao, Simon Billinge and members of the Billinge group.
8
+ #
9
+ # See GitHub contributions for a more detailed list of contributors.
10
+ # https://github.com/diffpy/diffpy.apps/graphs/contributors
11
+ #
12
+ # See LICENSE.rst for license information.
13
+ #
14
+ ##############################################################################
15
+ """User applications to help with tasks using diffpy packages."""
16
+
17
+ # package version
18
+ from diffpy.apps.version import __version__ # noqa
19
+
20
+ # silence the pyflakes syntax checker
21
+ assert __version__ or True
22
+
23
+ # End of file
@@ -0,0 +1,35 @@
1
+ import shutil
2
+ import subprocess
3
+ import tempfile
4
+ from pathlib import Path
5
+
6
+ REPO_URL = "https://github.com/diffpy/cmi-agent-skills"
7
+ DIR_NAME = "cmi-skill"
8
+
9
+
10
+ def agentify(args):
11
+ agent = args.agent
12
+ system_flag = args.system
13
+ if agent == "claude":
14
+ skills_dir = ".claude/skills"
15
+ elif agent == "codex":
16
+ skills_dir = ".codex/skills"
17
+ if system_flag:
18
+ destination = Path().home() / skills_dir / DIR_NAME
19
+ else:
20
+ destination = Path().cwd() / skills_dir / DIR_NAME
21
+ if destination.exists() and not args.update:
22
+ raise FileExistsError(
23
+ f"Agentic skill {DIR_NAME} already exists at {destination}. "
24
+ "To overwrite, pass '--update' flag to update the skill"
25
+ )
26
+ with tempfile.TemporaryDirectory() as tmp:
27
+ tmp_path = Path(tmp)
28
+ subprocess.run(
29
+ ["git", "clone", REPO_URL, str(tmp_path)],
30
+ check=True,
31
+ )
32
+ if destination.exists():
33
+ shutil.rmtree(destination)
34
+ shutil.copytree(tmp_path / DIR_NAME, destination, dirs_exist_ok=True)
35
+ print(f"Agentic skill {DIR_NAME} has been deployed to {destination}")
@@ -0,0 +1,298 @@
1
+ import inspect
2
+ from collections import OrderedDict
3
+ from pathlib import Path
4
+
5
+ import yaml
6
+ from textx import metamodel_from_str
7
+
8
+ from diffpy.apps.pdfadapter import PDFAdapter
9
+
10
+ grammar = r"""
11
+ Program:
12
+ commands*=Command
13
+ variable=VariableBlock?
14
+ ;
15
+
16
+ Command:
17
+ LoadCommand | SetCommand | CreateCommand | SaveCommand
18
+ ;
19
+
20
+ LoadCommand:
21
+ 'load' component=ID name=ID 'from' source=STRING
22
+ ;
23
+
24
+ SetCommand:
25
+ 'set' name=ID attribute=ID 'as' value+=Value[eolterm]
26
+ | 'set' name=ID 'as' value+=Value[eolterm]
27
+ ;
28
+
29
+ CreateCommand:
30
+ 'create' 'equation' 'variables' value+=Value[eolterm]
31
+ ;
32
+
33
+ SaveCommand:
34
+ 'save' 'to' source=STRING
35
+ ;
36
+
37
+ VariableBlock:
38
+ 'variables:' '---' content=/[\s\S]*?(?=---)/ '---'
39
+ ;
40
+
41
+ Value:
42
+ STRICTFLOAT | INT | STRING | RawValue
43
+ ;
44
+
45
+ RawValue:
46
+ /[^\s]+/
47
+ ;
48
+
49
+
50
+ Comment: /#[^\n]*/;
51
+ """
52
+
53
+
54
+ class MacroParser:
55
+ def __init__(self):
56
+ self.pdfadapter = PDFAdapter()
57
+ self.meta_model = metamodel_from_str(grammar)
58
+ self.meta_model.register_obj_processors(
59
+ {
60
+ "SetCommand": self.set_command_processor,
61
+ "LoadCommand": self.load_command_processor,
62
+ "VariableBlock": self.parameter_block_processor,
63
+ "CreateCommand": self.create_command_processor,
64
+ "SaveCommand": self.save_command_processor,
65
+ }
66
+ )
67
+ # key: method_name.argument_name
68
+ # value: argument_value
69
+ self.inputs = {}
70
+ # key: structure name or profile name set in the macro
71
+ # value: 'structure' or 'profile'
72
+ self.variables = OrderedDict()
73
+
74
+ def parse(self, code):
75
+ self.meta_model.model_from_str(code)
76
+
77
+ def input_as_list(self, key, value):
78
+ if key in self.inputs:
79
+ if not isinstance(self.inputs[key], list):
80
+ self.inputs[key] = [self.inputs[key]]
81
+ else:
82
+ self.inputs[key].append(value)
83
+ else:
84
+ if isinstance(value, list):
85
+ self.inputs[key] = value
86
+ else:
87
+ self.inputs[key] = [value]
88
+
89
+ def load_command_processor(self, command):
90
+ if command.component == "structure":
91
+ # TODO: support multiple structures input in the future
92
+ if (
93
+ self.inputs.get("initialize_structures.structure_paths")
94
+ is not None
95
+ ):
96
+ raise ValueError(
97
+ "Multiple structures are not supported by `runmacro` yet. "
98
+ "Please use python script instead."
99
+ )
100
+ key = "initialize_structures.structure_paths"
101
+ variable = "structure"
102
+ elif command.component == "profile":
103
+ if self.inputs.get("initialize_profile.profile_path") is not None:
104
+ raise ValueError(
105
+ "Multiple profiles are not supported by `runmacro`. "
106
+ "Please use python script instead."
107
+ )
108
+ key = "initialize_profile.profile_path"
109
+ variable = "profile"
110
+ else:
111
+ raise ValueError(
112
+ f"Unknown component type: {command.component} "
113
+ "Please use 'structure' or 'profile'."
114
+ )
115
+ source_path = Path(command.source)
116
+ if not source_path.exists():
117
+ raise FileNotFoundError(
118
+ f"{command.component} {source_path} not found. "
119
+ "Please ensure the path is correct and the file exists."
120
+ )
121
+ self.inputs[key] = str(source_path)
122
+ self.variables[command.name] = variable
123
+ if variable == "structure":
124
+ self.input_as_list("initialize_structures.names", command.name)
125
+
126
+ def set_command_processor(self, command):
127
+ if command.name == "equation":
128
+ key = "initialize_contribution.equation"
129
+ elif command.name in self.variables:
130
+ if self.variables[command.name] == "structure":
131
+ if command.attribute == "spacegroup":
132
+ key = "initialize_structures.spacegroups"
133
+ else:
134
+ key = "initialize_structures." + command.attribute
135
+ elif self.variables[command.name] == "profile":
136
+ key = "initialize_profile." + command.attribute
137
+ else:
138
+ raise ValueError(
139
+ f"Unknown variable type for name: {command.name}. "
140
+ "This is an internal error. Please report this issue to "
141
+ "the developers."
142
+ )
143
+ else:
144
+ raise ValueError(
145
+ f"Unknown name in set command: {command.name}. "
146
+ "Please ensure that it is typed correctly as 'equation' or "
147
+ "it matches a previously loaded structure or "
148
+ "profile. "
149
+ )
150
+ self.input_as_list(key, command.value)
151
+
152
+ def parameter_block_processor(self, variable_block):
153
+ self.inputs["set_initial_variable_values.variable_name_to_value"] = {}
154
+ self.inputs["refine_variables.variable_names"] = []
155
+ parameters = yaml.safe_load(variable_block.content)
156
+ if not isinstance(parameters, list):
157
+ raise ValueError(
158
+ "Parameter block should contain a list of parameters. "
159
+ "Please use the following format:\n"
160
+ "- param1 # use default initial value\n"
161
+ "- param2: initial_value\n"
162
+ )
163
+ for item in parameters:
164
+ if isinstance(item, str):
165
+ self.inputs["refine_variables.variable_names"].append(
166
+ item.replace(".", "_")
167
+ )
168
+ elif isinstance(item, dict):
169
+ pname, pvalue = list(item.items())[0]
170
+ self.inputs[
171
+ "set_initial_variable_values.variable_name_to_value"
172
+ ][pname.replace(".", "_")] = pvalue
173
+ self.inputs["refine_variables.variable_names"].append(
174
+ pname.replace(".", "_")
175
+ )
176
+ else:
177
+ raise ValueError(
178
+ "Variables block items are not correctly formatted. "
179
+ "Please use the following format:\n"
180
+ "- param1 # use default initial value\n"
181
+ "- param2: initial_value\n"
182
+ )
183
+
184
+ def create_command_processor(self, command):
185
+ self.inputs["add_contribution_variables.variable_names"] = (
186
+ command.value
187
+ )
188
+
189
+ def save_command_processor(self, command):
190
+ self.inputs["save_results.result_path"] = command.source
191
+
192
+ def required_args(self, func):
193
+ sig = inspect.signature(func)
194
+ return [
195
+ name
196
+ for name, p in sig.parameters.items()
197
+ if p.default is inspect.Parameter.empty
198
+ and p.kind
199
+ in (
200
+ inspect.Parameter.POSITIONAL_ONLY,
201
+ inspect.Parameter.POSITIONAL_OR_KEYWORD,
202
+ inspect.Parameter.KEYWORD_ONLY,
203
+ )
204
+ ]
205
+
206
+ def call_pdfadapter_method(self, method_name, function_requirement):
207
+ func = getattr(self.pdfadapter, method_name)
208
+ required_arguments = self.required_args(func)
209
+ arguments = {
210
+ key.split(".")[1]: value
211
+ for key, value in self.inputs.items()
212
+ if key.startswith(method_name)
213
+ }
214
+ if not all(arg in arguments for arg in required_arguments):
215
+ missing_args = [
216
+ arg for arg in required_arguments if arg not in arguments
217
+ ]
218
+ if function_requirement == "required":
219
+ raise ValueError(
220
+ "Missing required arguments for function "
221
+ f"'{method_name}' {', '.join(missing_args)}. "
222
+ "Please provide these arguments in the macro file."
223
+ )
224
+ elif function_requirement == "optional":
225
+ print(
226
+ "Missing required arguments for function "
227
+ f"'{method_name}' {', '.join(missing_args)}. "
228
+ "This function will be skipped. "
229
+ "Please provide these arguments in the macro file "
230
+ "to activate this function."
231
+ )
232
+ return
233
+ func(**arguments)
234
+
235
+ def preprocess(self):
236
+ methods_to_call = [
237
+ ("initialize_profile", "required"),
238
+ ("initialize_structures", "required"),
239
+ ("initialize_contribution", "required"),
240
+ ("initialize_recipe", "required"),
241
+ ("add_contribution_variables", "optional"),
242
+ ("set_initial_variable_values", "optional"),
243
+ ]
244
+ for method in methods_to_call:
245
+ self.call_pdfadapter_method(*method)
246
+
247
+ def run(self):
248
+ methods_to_call = [
249
+ ("refine_variables", "required"),
250
+ ("save_results", "optional"),
251
+ ]
252
+ for method in methods_to_call:
253
+ self.call_pdfadapter_method(*method)
254
+ return self.pdfadapter.get_results()
255
+
256
+
257
+ def runmacro(args):
258
+ dpin_path = Path(args.file)
259
+ if not dpin_path.exists():
260
+ raise FileNotFoundError(
261
+ f"{str(dpin_path)} not found. Please check if this file "
262
+ "exists and provide the correct path to it."
263
+ )
264
+ dsl_code = dpin_path.read_text()
265
+ parser = MacroParser()
266
+ parser.parse(dsl_code)
267
+ parser.preprocess()
268
+ return parser.run()
269
+
270
+
271
+ if __name__ == "__main__":
272
+ parser = MacroParser()
273
+ code = f"""
274
+ load structure G1 from "{str(Path(__file__).parents[3] / "tests/data/Ni.cif")}"
275
+ load profile exp_ni from "{str(Path(__file__).parents[3] / "tests/data/Ni.gr")}"
276
+
277
+ set G1 spacegroup as auto
278
+ set exp_ni q_range as 0.1 25
279
+ set exp_ni calculation_range as 1.5 50 0.01
280
+ create equation variables s0
281
+ set equation as "s0*G1"
282
+ save to "results.json"
283
+
284
+ variables:
285
+ ---
286
+ - G1.a: 3.52
287
+ - s0: 0.4
288
+ - G1.Uiso_0: 0.005
289
+ - G1.delta2: 2
290
+ - qdamp: 0.04
291
+ - qbroad: 0.02
292
+ ---
293
+ """ # noqa: E501
294
+ parser.parse(code)
295
+ parser.preprocess()
296
+ recipe = parser.pdfadapter.recipe
297
+ for pname, param in recipe._parameters.items():
298
+ print(f"{pname}: {param.value}")
diffpy/apps/apps.py ADDED
@@ -0,0 +1,81 @@
1
+ import argparse
2
+
3
+ from diffpy.apps.app_agentify import agentify
4
+ from diffpy.apps.app_runmacro import runmacro
5
+ from diffpy.apps.version import __version__ # noqa
6
+
7
+
8
+ class DiffpyHelpFormatter(argparse.RawDescriptionHelpFormatter):
9
+ """Format subcommands without showing an extra placeholder entry."""
10
+
11
+ def _format_action(self, action):
12
+ if isinstance(action, argparse._SubParsersAction):
13
+ return "".join(
14
+ self._format_action(subaction)
15
+ for subaction in self._iter_indented_subactions(action)
16
+ )
17
+ return super()._format_action(action)
18
+
19
+
20
+ def main():
21
+ parser = argparse.ArgumentParser(
22
+ prog="diffpy.apps",
23
+ description=(
24
+ "User applications to help with tasks using diffpy packages\n\n"
25
+ "For more information, visit: "
26
+ "https://github.com/diffpy/diffpy.apps/"
27
+ ),
28
+ formatter_class=DiffpyHelpFormatter,
29
+ )
30
+
31
+ parser.add_argument(
32
+ "--version",
33
+ action="version",
34
+ version=f"diffpy.apps {__version__}",
35
+ )
36
+ apps_parsers = parser.add_subparsers(
37
+ title="Available applications",
38
+ dest="application",
39
+ )
40
+ # runmacro application
41
+ runmacro_parser = apps_parsers.add_parser(
42
+ "runmacro",
43
+ help="Run a macro `<.dp-in>` file",
44
+ )
45
+ runmacro_parser.add_argument(
46
+ "file",
47
+ type=str,
48
+ help="Path to the `<.dp-in>` macro file to be run",
49
+ )
50
+ runmacro_parser.set_defaults(func=runmacro)
51
+ # agentify application
52
+ agentify_parser = apps_parsers.add_parser(
53
+ "agentify",
54
+ help="Deploy diffpy.cmi agentic skills in the local environment.",
55
+ )
56
+ agentify_parser.add_argument(
57
+ "--agent",
58
+ help="The agent to use for the agentic skill.",
59
+ default="claude",
60
+ choices=["claude", "codex"],
61
+ )
62
+ agentify_parser.add_argument(
63
+ "--update",
64
+ action="store_true",
65
+ help="When set, update the existing agentic skill.",
66
+ )
67
+ agentify_parser.add_argument(
68
+ "--system",
69
+ action="store_true",
70
+ help="When set, deploy the agentic skill to the system directory.",
71
+ )
72
+ agentify_parser.set_defaults(func=agentify)
73
+ args = parser.parse_args()
74
+ if args.application is None:
75
+ parser.print_help()
76
+ else:
77
+ args.func(args)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ main()
@@ -0,0 +1,341 @@
1
+ import json
2
+ import warnings
3
+ from pathlib import Path
4
+
5
+ import numpy
6
+ from scipy.optimize import least_squares
7
+
8
+ from diffpy.srfit.fitbase import (
9
+ FitContribution,
10
+ FitRecipe,
11
+ FitResults,
12
+ Profile,
13
+ )
14
+ from diffpy.srfit.pdf import PDFGenerator, PDFParser
15
+ from diffpy.srfit.structure import constrainAsSpaceGroup
16
+ from diffpy.structure.parsers import getParser
17
+
18
+
19
+ class PDFAdapter:
20
+ """Build and configure a PDF fitting workflow backed by
21
+ diffpy.srfit.
22
+
23
+ The adapter assembles the profile, structure generators, contribution,
24
+ and recipe objects required for a PDF refinement, then exposes the fit
25
+ results in a JSON-compatible dictionary.
26
+
27
+ Public methods
28
+ --------------
29
+ initialize_profile(profile_path: str, q_range=None, calculation_range=None)
30
+ Load the experimental PDF profile.
31
+ initialize_structures(
32
+ structure_paths: list[str], run_parallel=True, spacegroups=None,
33
+ names=None
34
+ )
35
+ Load structures and create PDFGenerator objects for them.
36
+ initialize_contribution(equation_string=None)
37
+ Create the FitContribution from the loaded profile and generators.
38
+ initialize_recipe()
39
+ Create the FitRecipe for the current contribution.
40
+ set_initial_variable_values(variable_name_to_value: dict)
41
+ Set recipe parameter values by name.
42
+ get_results()
43
+ Return the current fit results as a JSON-compatible dictionary.
44
+ """
45
+
46
+ def initialize_profile(
47
+ self,
48
+ profile_path: str,
49
+ q_range=None,
50
+ calculation_range=None,
51
+ ):
52
+ """Load the experimental PDF profile.
53
+
54
+ Parameters
55
+ ----------
56
+ profile_path : str
57
+ The path to the experimental PDF profile file.
58
+ q_range : sequence of float, optional
59
+ The two-element sequence containing qmin and qmax. When omitted,
60
+ the values parsed from the profile file are used.
61
+ calculation_range : list or tuple of three floats, optional
62
+ The three-element sequence or mapping defining xmin, xmax, and dx
63
+ for the calculation range. When omitted, the range parsed from the
64
+ profile file is used.
65
+ """
66
+ profile = Profile()
67
+ parser = PDFParser()
68
+ parser.parseString(Path(profile_path).read_text())
69
+ profile.loadParsedData(parser)
70
+ if q_range is not None:
71
+ profile.meta["qmin"] = q_range[0]
72
+ profile.meta["qmax"] = q_range[1]
73
+ if calculation_range is not None:
74
+ if isinstance(calculation_range, (list, tuple)):
75
+ calculation_range = {
76
+ "xmin": calculation_range[0],
77
+ "xmax": calculation_range[1],
78
+ "dx": calculation_range[2],
79
+ }
80
+ profile.setCalculationRange(**calculation_range)
81
+ self.profile = profile
82
+
83
+ def initialize_structures(
84
+ self,
85
+ structure_paths: list[str],
86
+ run_parallel=True,
87
+ spacegroups=None,
88
+ names=None,
89
+ ):
90
+ """Load structures and create PDFGenerator objects for them.
91
+
92
+ This method should be called after initialize_profile.
93
+
94
+ Parameters
95
+ ----------
96
+ structure_paths : list of str
97
+ The paths to structure files in CIF format.
98
+ run_parallel : bool
99
+ If True, enable generator parallelization when the optional
100
+ dependencies are available.
101
+ spacegroups : list of str or None
102
+ The space groups are inferred from the parsed CIF
103
+ files when available and otherwise default to "P1". They will be
104
+ used to impose symmetry constraints on the structure parameters
105
+ during fitting.
106
+ names : list of str or None
107
+ The names assigned to the generators. Missing names default to
108
+ "G1", "G2", and so on.
109
+ """
110
+ if isinstance(structure_paths, str):
111
+ structure_paths = [structure_paths]
112
+ structures = []
113
+ have_spacegroups = False
114
+ if spacegroups is not None:
115
+ if len(spacegroups) != len(structure_paths):
116
+ raise ValueError(
117
+ f"spacegroups list {spacegroups} must match "
118
+ f"structure_paths list {structure_paths}. "
119
+ "Please provide a space group for each structure or set "
120
+ "spacegroups to None to infer them automatically."
121
+ )
122
+ else:
123
+ have_spacegroups = True
124
+ if not have_spacegroups:
125
+ spacegroups = []
126
+ pdfgenerators = []
127
+ if run_parallel:
128
+ try:
129
+ import multiprocessing
130
+ from multiprocessing import Pool
131
+
132
+ import psutil
133
+
134
+ syst_cores = multiprocessing.cpu_count()
135
+ cpu_percent = psutil.cpu_percent()
136
+ avail_cores = numpy.floor(
137
+ (100 - cpu_percent) / (100.0 / syst_cores)
138
+ )
139
+ ncpu = int(numpy.max([1, avail_cores]))
140
+ pool = Pool(processes=ncpu)
141
+ self.pool = pool
142
+ except ImportError:
143
+ warnings.warn(
144
+ "\nYou don't appear to have the necessary packages for "
145
+ "parallelization. Proceeding without parallelization."
146
+ )
147
+ run_parallel = False
148
+ for i, structure_path in enumerate(structure_paths):
149
+ name = names[i] if names and i < len(names) else f"G{i+1}"
150
+ stru_parser = getParser("cif")
151
+ structure = stru_parser.parse(Path(structure_path).read_text())
152
+ sg = getattr(stru_parser, "spacegroup", None)
153
+ spacegroup = sg.short_name if sg is not None else "P1"
154
+ structures.append(structure)
155
+ if not have_spacegroups:
156
+ spacegroups.append(spacegroup)
157
+ else:
158
+ if spacegroups[i] == "auto":
159
+ spacegroups[i] = spacegroup
160
+ pdfgenerator = PDFGenerator(name)
161
+ pdfgenerator.setStructure(structure)
162
+ if run_parallel:
163
+ pdfgenerator.parallel(ncpu=ncpu, mapfunc=self.pool.map)
164
+ pdfgenerators.append(pdfgenerator)
165
+ self.spacegroups = spacegroups
166
+ self.pdfgenerators = pdfgenerators
167
+
168
+ def initialize_contribution(self, equation=None):
169
+ """Create the FitContribution from the loaded profile and
170
+ generators.
171
+
172
+ This method should be called after initialize_profile and
173
+ initialize_structures.
174
+
175
+ Parameters
176
+ ----------
177
+ equation : list of str, optional
178
+ The list of a single equation passed to
179
+ FitContribution.setEquation.
180
+
181
+ Returns
182
+ -------
183
+ FitContribution
184
+ The configured contribution object.
185
+ """
186
+ equation = equation[0] if equation is not None else None
187
+ contribution = FitContribution("pdfcontribution")
188
+ contribution.setProfile(self.profile)
189
+ for pdfgenerator in self.pdfgenerators:
190
+ contribution.addProfileGenerator(pdfgenerator)
191
+ contribution.setEquation(equation)
192
+ self.contribution = contribution
193
+ return self.contribution
194
+
195
+ def initialize_recipe(
196
+ self,
197
+ ):
198
+ """Create the FitRecipe for the current contribution.
199
+
200
+ This method should be called after initialize_contribution. The
201
+ recipe includes shared qdamp and qbroad variables, per-generator
202
+ delta1 and delta2 variables, and structure parameters
203
+ constrained by the space group.
204
+ """
205
+
206
+ recipe = FitRecipe()
207
+ recipe.addContribution(self.contribution)
208
+ qdamp = recipe.newVar("qdamp", fixed=False, value=0.04)
209
+ qbroad = recipe.newVar("qbroad", fixed=False, value=0.02)
210
+ for i, (pdfgenerator, spacegroup) in enumerate(
211
+ zip(self.pdfgenerators, self.spacegroups)
212
+ ):
213
+ for pname in [
214
+ "delta1",
215
+ "delta2",
216
+ ]:
217
+ par = getattr(pdfgenerator, pname)
218
+ recipe.addVar(
219
+ par, name=f"{pdfgenerator.name}_{pname}", fixed=False
220
+ )
221
+ recipe.constrain(pdfgenerator.qdamp, qdamp)
222
+ recipe.constrain(pdfgenerator.qbroad, qbroad)
223
+ stru_parset = pdfgenerator.phase
224
+ spacegroupparams = constrainAsSpaceGroup(stru_parset, spacegroup)
225
+ for par in spacegroupparams.xyzpars:
226
+ recipe.addVar(
227
+ par, name=f"{pdfgenerator.name}_{par.name}", fixed=False
228
+ )
229
+ for par in spacegroupparams.latpars:
230
+ recipe.addVar(
231
+ par, name=f"{pdfgenerator.name}_{par.name}", fixed=False
232
+ )
233
+ for par in spacegroupparams.adppars:
234
+ recipe.addVar(
235
+ par, name=f"{pdfgenerator.name}_{par.name}", fixed=False
236
+ )
237
+ recipe.fithooks[0].verbose = 0
238
+ self.recipe = recipe
239
+
240
+ def add_contribution_variables(self, variable_names):
241
+ """Add contribution parameters to the recipe.
242
+
243
+ Parameters
244
+ ----------
245
+ variable_names : list of str
246
+ The names of the variables to be added to the recipe.
247
+ e.g. 's0' for scale factor.
248
+ """
249
+ for var_name in variable_names:
250
+ self.recipe.addVar(
251
+ getattr(self.contribution, var_name),
252
+ name=var_name,
253
+ fixed=False,
254
+ )
255
+
256
+ def refine_variables(self, variable_names):
257
+ """Refine the specified variables.
258
+
259
+ Parameters
260
+ ----------
261
+ variable_names : list of str
262
+ The names of the variables to be refined.
263
+ """
264
+ self.recipe.fix("all")
265
+ for var in variable_names:
266
+ self.recipe.free(var)
267
+ least_squares(self.recipe.residual, self.recipe.values)
268
+
269
+ def set_initial_variable_values(self, variable_name_to_value: dict):
270
+ """Set recipe parameter values by name.
271
+
272
+ Parameters
273
+ ----------
274
+ variable_name_to_value : dict
275
+ Mapping from recipe variable names to new values.
276
+ """
277
+ for vname, vvalue in variable_name_to_value.items():
278
+ self.recipe._parameters[vname].setValue(vvalue)
279
+
280
+ def get_results(self):
281
+ """Return the current fit results as a JSON-compatible
282
+ dictionary.
283
+
284
+ Returns
285
+ -------
286
+ dict
287
+ Residual statistics, variable values, constraints, the covariance
288
+ matrix, and a certainty flag.
289
+ """
290
+ fit_results = FitResults(self.recipe)
291
+ results_dict = {}
292
+ results_dict["residual"] = fit_results.residual
293
+ results_dict["contributions"] = (
294
+ fit_results.residual - fit_results.penalty
295
+ )
296
+ results_dict["restraints"] = fit_results.penalty
297
+ results_dict["chi2"] = fit_results.chi2
298
+ results_dict["reduced_chi2"] = fit_results.rchi2
299
+ results_dict["rw"] = fit_results.rw
300
+ # variables
301
+ results_dict["variables"] = {}
302
+ for name, val, unc in zip(
303
+ fit_results.varnames, fit_results.varvals, fit_results.varunc
304
+ ):
305
+ results_dict["variables"][name] = {
306
+ "value": val,
307
+ "uncertainty": unc,
308
+ }
309
+ # fixed variables
310
+ results_dict["fixed_variables"] = {}
311
+ if fit_results.fixednames is not None:
312
+ for name, val in zip(
313
+ fit_results.fixednames, fit_results.fixedvals
314
+ ):
315
+ results_dict["fixed_variables"][name] = {"value": val}
316
+ # constraints
317
+ results_dict["constraints"] = {}
318
+ if fit_results.connames and fit_results.showcon:
319
+ for con in fit_results.conresults.values():
320
+ for i, loc in enumerate(con.conlocs):
321
+ names = [obj.name for obj in loc]
322
+ name = ".".join(names)
323
+ val = con.convals[i]
324
+ unc = con.conuncs[i]
325
+ results_dict["constraints"][name] = {
326
+ "value": val,
327
+ "uncertainty": unc,
328
+ }
329
+ # covariance matrix
330
+ results_dict["covariance_matrix"] = fit_results.cov.tolist()
331
+ # certainty
332
+ certain = True
333
+ for con in fit_results.conresults.values():
334
+ if (con.dy == 1).all():
335
+ certain = False
336
+ results_dict["certain"] = certain
337
+ return results_dict
338
+
339
+ def save_results(self, result_path):
340
+ with open(result_path, "w") as f:
341
+ json.dump(self.get_results(), f, indent=4)
diffpy/apps/version.py ADDED
@@ -0,0 +1,26 @@
1
+ #!/usr/bin/env python
2
+ ##############################################################################
3
+ #
4
+ # (c) 2026 diffpy.apps contributors.
5
+ # All rights reserved.
6
+ #
7
+ # File coded by: Yuchen Xiao, Simon Billinge and members of the Billinge group.
8
+ #
9
+ # See GitHub contributions for a more detailed list of contributors.
10
+ # https://github.com/diffpy/diffpy.apps/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("diffpy.apps")
25
+ except PackageNotFoundError:
26
+ __version__ = "unknown"
@@ -0,0 +1,178 @@
1
+ Metadata-Version: 2.4
2
+ Name: diffpy.apps
3
+ Version: 0.1.0
4
+ Summary: User applications to help with tasks using diffpy packages
5
+ Author-email: Yuchen Xiao <yx2924@columbia.edu>, Simon Billinge <sbillinge@ucsb.edu>
6
+ Maintainer-email: Yuchen Xiao <yx2924@columbia.edu>, Simon Billinge <sbillinge@ucsb.edu>
7
+ Project-URL: Homepage, https://github.com/diffpy/diffpy.apps/
8
+ Project-URL: Issues, https://github.com/diffpy/diffpy.apps/issues/
9
+ Keywords: diffraction,PDF,X-ray,neutron
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.11
20
+ Classifier: Programming Language :: Python :: 3.12
21
+ Classifier: Programming Language :: Python :: 3.13
22
+ Classifier: Topic :: Scientific/Engineering :: Physics
23
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
24
+ Requires-Python: <3.14,>=3.11
25
+ Description-Content-Type: text/x-rst
26
+ License-File: LICENSE.rst
27
+ License-File: AUTHORS.rst
28
+ Requires-Dist: numpy
29
+ Dynamic: license-file
30
+
31
+ |Icon| |title|_
32
+ ===============
33
+
34
+ .. |title| replace:: diffpy.apps
35
+ .. _title: https://diffpy.github.io/diffpy.apps
36
+
37
+ .. |Icon| image:: https://avatars.githubusercontent.com/diffpy
38
+ :target: https://diffpy.github.io/diffpy.apps
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/diffpy/diffpy.apps/actions/workflows/matrix-and-codecov-on-merge-to-main.yml/badge.svg
49
+ :target: https://github.com/diffpy/diffpy.apps/actions/workflows/matrix-and-codecov-on-merge-to-main.yml
50
+
51
+ .. |Codecov| image:: https://codecov.io/gh/diffpy/diffpy.apps/branch/main/graph/badge.svg
52
+ :target: https://codecov.io/gh/diffpy/diffpy.apps
53
+
54
+ .. |Forge| image:: https://img.shields.io/conda/vn/conda-forge/diffpy.apps
55
+ :target: https://anaconda.org/conda-forge/diffpy.apps
56
+
57
+ .. |PR| image:: https://img.shields.io/badge/PR-Welcome-29ab47ff
58
+ :target: https://github.com/diffpy/diffpy.apps/pulls
59
+
60
+ .. |PyPI| image:: https://img.shields.io/pypi/v/diffpy.apps
61
+ :target: https://pypi.org/project/diffpy.apps/
62
+
63
+ .. |PythonVersion| image:: https://img.shields.io/pypi/pyversions/diffpy.apps
64
+ :target: https://pypi.org/project/diffpy.apps/
65
+
66
+ .. |Tracking| image:: https://img.shields.io/badge/issue_tracking-github-blue
67
+ :target: https://github.com/diffpy/diffpy.apps/issues
68
+
69
+ User applications to help with tasks using diffpy packages. Currently it
70
+ contains
71
+
72
+ - `runmacro`: A runner for DiffPy macro files.
73
+ - `agentify`: A deployer for diffpy.cmi agentic skills.
74
+
75
+ For more information about the diffpy.apps library, please consult our `online documentation <https://diffpy.github.io/diffpy.apps>`_.
76
+
77
+ Citation
78
+ --------
79
+
80
+ If you use diffpy.apps in a scientific publication, we would like you to cite this package as
81
+
82
+ diffpy.apps Package, https://github.com/diffpy/diffpy.apps
83
+
84
+ Installation
85
+ ------------
86
+
87
+ The preferred method is to use `Miniconda Python
88
+ <https://docs.conda.io/projects/miniconda/en/latest/miniconda-install.html>`_
89
+ and install from the "conda-forge" channel of Conda packages.
90
+
91
+ To add "conda-forge" to the conda channels, run the following in a terminal. ::
92
+
93
+ conda config --add channels conda-forge
94
+
95
+ We want to install our packages in a suitable conda environment.
96
+ The following creates and activates a new environment named ``diffpy.apps_env`` ::
97
+
98
+ conda create -n diffpy.apps_env diffpy.apps python=3.13
99
+ conda activate diffpy.apps_env
100
+
101
+ The output should print the latest version displayed on the badges above.
102
+
103
+ If the above does not work, you can use ``pip`` to download and install the latest release from
104
+ `Python Package Index <https://pypi.python.org>`_.
105
+
106
+ You still need to install some of its dependencies from ``conda`` ::
107
+
108
+ git clone https://github.com/diffpy/diffpy.apps.git
109
+ cd diffpy.apps
110
+ conda activate diffpy.apps_env
111
+ conda install --file requirements/conda.txt
112
+
113
+
114
+ To install using ``pip`` into your ``diffpy.apps_env`` environment, type ::
115
+
116
+ pip install diffpy.apps
117
+
118
+ If you prefer to install from sources, after installing the dependencies, obtain the source archive from
119
+ `GitHub <https://github.com/diffpy/diffpy.apps/>`_. Once installed, ``cd`` into your ``diffpy.apps`` directory
120
+ and run the following ::
121
+
122
+ pip install .
123
+
124
+ This package also provides command-line utilities. To check the software has been installed correctly, type ::
125
+
126
+ diffpy.apps --version
127
+
128
+ You can also type the following command to verify the installation. ::
129
+
130
+ python -c "import diffpy.apps; print(diffpy.apps.__version__)"
131
+
132
+
133
+ To view the basic usage and available commands, type ::
134
+
135
+ diffpy.apps -h
136
+
137
+ Getting Started
138
+ ---------------
139
+
140
+ You may consult our `online documentation <https://diffpy.github.io/diffpy.apps>`_ for tutorials and API references.
141
+
142
+ Support and Contribute
143
+ ----------------------
144
+
145
+ If you see a bug or want to request a feature, please `report it as an issue <https://github.com/diffpy/diffpy.apps/issues>`_ and/or `submit a fix as a PR <https://github.com/diffpy/diffpy.apps/pulls>`_.
146
+
147
+ Feel free to fork the project and contribute. To install diffpy.apps
148
+ in a development mode, with its sources being directly used by Python
149
+ rather than copied to a package directory, use the following in the root
150
+ directory ::
151
+
152
+ pip install -e .
153
+
154
+ To ensure code quality and to prevent accidental commits into the default branch, please set up the use of our pre-commit
155
+ hooks.
156
+
157
+ 1. Install pre-commit in your working environment by running ``conda install pre-commit``.
158
+
159
+ 2. Initialize pre-commit (one time only) ``pre-commit install``.
160
+
161
+ Thereafter your code will be linted by black and isort and checked against flake8 before you can commit.
162
+ If it fails by black or isort, just rerun and it should pass (black and isort will modify the files so should
163
+ pass after they are modified). If the flake8 test fails please see the error messages and fix them manually before
164
+ trying to commit again.
165
+
166
+ Improvements and fixes are always appreciated.
167
+
168
+ Before contributing, please read our `Code of Conduct <https://github.com/diffpy/diffpy.apps/blob/main/CODE-OF-CONDUCT.rst>`_.
169
+
170
+ Contact
171
+ -------
172
+
173
+ For more information on diffpy.apps please visit the project `web-page <https://diffpy.github.io/>`_ or email the maintainers ``Yuchen Xiao(yx2924@columbia.edu) and Simon Billinge(sbillinge@ucsb.edu)``.
174
+
175
+ Acknowledgements
176
+ ----------------
177
+
178
+ ``diffpy.apps`` is built and maintained with `scikit-package <https://scikit-package.github.io/scikit-package/>`_.
@@ -0,0 +1,14 @@
1
+ diffpy/__init__.py,sha256=A_tOI_1lETnfUo1dOfJIONN5aofysoAQ_ZGsYxsAfMI,525
2
+ diffpy/apps/__init__.py,sha256=XsuN1OGd0KAO2ultaC3sMepmjS0S29E0DmC7m1iGgJs,720
3
+ diffpy/apps/app_agentify.py,sha256=MiqRXEkFoNGl108ig9iMzmKe1RWS7oBylX-dfAx1M_0,1151
4
+ diffpy/apps/app_runmacro.py,sha256=4I0mOToOcaroRyIDV_vJr7vkCtkPapCp5zhJYWjTbyQ,10144
5
+ diffpy/apps/apps.py,sha256=xJ161akAKLj9ZwdpfYOQ0jgtsABfcq6YaZi_Y3toowc,2394
6
+ diffpy/apps/pdfadapter.py,sha256=HxiROGXmj1I5_gNDx5xrswsNwiDDMFZd5X6H-oS43RE,12732
7
+ diffpy/apps/version.py,sha256=_B5JoKsrDgIoVd7YfbUt_QnZbFto9XFOgZFRYfu0XgA,898
8
+ diffpy_apps-0.1.0.dist-info/licenses/AUTHORS.rst,sha256=FGOWUsPNZ9m4ss1MD_wVMRR4tDpgqQeGnQdtTnq4u64,199
9
+ diffpy_apps-0.1.0.dist-info/licenses/LICENSE.rst,sha256=BnJcVQf_9vCrIUVdJ3LxMD5S3cjQjuiOeyGReFV8bqQ,1533
10
+ diffpy_apps-0.1.0.dist-info/METADATA,sha256=jq6Vy5Cq_pI6L-MRazvddroi4Qt5z_p3ElYKw-VfqSM,6957
11
+ diffpy_apps-0.1.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
12
+ diffpy_apps-0.1.0.dist-info/entry_points.txt,sha256=JXWRMH9NTZLJrtVkw6hVOrFJjCbR1y71ey7Dnv-40iU,89
13
+ diffpy_apps-0.1.0.dist-info/top_level.txt,sha256=34qLZyi9mxikpHepVDTMHrrNbe8VoOuz3ShYeiWA7KA,7
14
+ diffpy_apps-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ diffpy.app = diffpy.apps.apps:main
3
+ diffpy.apps = diffpy.apps.apps:main
@@ -0,0 +1,10 @@
1
+ Authors
2
+ =======
3
+
4
+ Yuchen Xiao, Simon Billinge and members of the Billinge group
5
+
6
+ Contributors
7
+ ------------
8
+
9
+ For a list of contributors, visit
10
+ https://github.com/diffpy/diffpy.apps/graphs/contributors
@@ -0,0 +1,29 @@
1
+ BSD 3-Clause License
2
+
3
+ Copyright (c) 2026, diffpy.apps contributors.
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
+ diffpy