Skip to content

Commit 606bcfc

Browse files
authored
Merge pull request #3 from ycexiao/add-dsl
feat: add `DSL` support
2 parents feb7e1f + aad7c3d commit 606bcfc

File tree

13 files changed

+7047
-51
lines changed

13 files changed

+7047
-51
lines changed

.github/workflows/tests-on-pr.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,6 @@ jobs:
1111
project: diffpy.apps
1212
c_extension: false
1313
headless: false
14+
python_version: 3.13
1415
secrets:
1516
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

news/add-dsl.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* Add `DSL` support.
4+
5+
**Changed:**
6+
7+
* <news item>
8+
9+
**Deprecated:**
10+
11+
* <news item>
12+
13+
**Removed:**
14+
15+
* <news item>
16+
17+
**Fixed:**
18+
19+
* <news item>
20+
21+
**Security:**
22+
23+
* <news item>

requirements/conda.txt

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,8 @@
11
numpy
2+
scipy
3+
psutil
4+
textx
5+
pyyaml
6+
diffpy.srfit
7+
diffpy.srreal
8+
diffpy.structure

src/diffpy/apps/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
# See LICENSE.rst for license information.
1313
#
1414
##############################################################################
15-
"""User applications to help with tasks using diffpy packages"""
15+
"""User applications to help with tasks using diffpy packages."""
1616

1717
# package version
1818
from diffpy.apps.version import __version__ # noqa
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
import json
2+
from pathlib import Path
3+
4+
import yaml
5+
from scipy.optimize import least_squares
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+
51+
class DiffpyInterpreter:
52+
def __init__(self):
53+
self.pdfadapter = PDFAdapter()
54+
self.meta_model = metamodel_from_str(grammar)
55+
self.meta_model.register_obj_processors(
56+
{
57+
"SetCommand": self.set_command_processor,
58+
"LoadCommand": self.load_command_processor,
59+
"VariableBlock": self.variable_block_processor,
60+
"CreateCommand": self.create_command_processor,
61+
"SaveCommand": self.save_command_processor,
62+
}
63+
)
64+
self.inputs = {}
65+
self.profile_name = ""
66+
self.structure_name = (
67+
"" # TODO: support multiple structures in the future
68+
)
69+
70+
def interpret(self, code):
71+
self.meta_model.model_from_str(code)
72+
73+
def load_command_processor(self, command):
74+
if command.component == "structure":
75+
source_entry = "structure_path"
76+
attribute_name = "structure_name"
77+
elif command.component == "profile":
78+
source_entry = "profile_path"
79+
attribute_name = "profile_name"
80+
else:
81+
raise ValueError(
82+
f"Unknown component type: {command.component} "
83+
"Please use 'structure' or 'profile'."
84+
)
85+
source_path = Path(command.source)
86+
if not source_path.exists():
87+
raise FileNotFoundError(
88+
f"{command.component} {source_path} not found. "
89+
"Please ensure the path is correct and the file exists."
90+
)
91+
self.inputs[source_entry] = str(source_path)
92+
setattr(self, attribute_name, command.name)
93+
94+
def set_command_processor(self, command):
95+
if "structures_config" not in self.inputs:
96+
self.inputs["structures_config"] = {}
97+
if "profiles_config" not in self.inputs:
98+
self.inputs["profiles_config"] = {}
99+
if command.name in ["equation"]:
100+
self.inputs[command.name] = command.value
101+
elif command.name == self.structure_name:
102+
self.inputs["structures_config"][
103+
self.structure_name + "_" + command.attribute
104+
] = command.value
105+
elif command.name == self.profile_name:
106+
self.inputs["profiles_config"][command.attribute] = command.value
107+
else:
108+
raise ValueError(
109+
f"Unknown name in set command: {command.name}. "
110+
"Please ensure it matches a previously loaded structure or "
111+
"profile."
112+
)
113+
114+
def variable_block_processor(self, variable_block):
115+
self.inputs["variables"] = []
116+
self.inputs["initial_values"] = {}
117+
variables = yaml.safe_load(variable_block.content)
118+
if not isinstance(variables, list):
119+
raise ValueError(
120+
"Variables block should contain a list of variables. "
121+
"Please use the following format:\n"
122+
"- var1 # use default initial value\n"
123+
"- var2: initial_value\n"
124+
)
125+
for item in variables:
126+
if isinstance(item, str):
127+
self.inputs["variables"].append(item.replace(".", "_"))
128+
elif isinstance(item, dict):
129+
pname, pvalue = list(item.items())[0]
130+
self.inputs["variables"].append(pname.replace(".", "_"))
131+
self.inputs["initial_values"][pname.replace(".", "_")] = pvalue
132+
else:
133+
raise ValueError(
134+
"Variables block items are not correctly formatted. "
135+
"Please use the following format:\n"
136+
"- var1 # use default initial value\n"
137+
"- var2: initial_value\n"
138+
)
139+
140+
def create_command_processor(self, command):
141+
self.inputs["equation_variable"] = [
142+
v for v in command.value if isinstance(v, str)
143+
]
144+
145+
def save_command_processor(self, command):
146+
self.inputs["result_path"] = command.source
147+
148+
def configure_adapter(self):
149+
self.pdfadapter.initialize_profile(
150+
self.inputs["profile_path"], **self.inputs["profiles_config"]
151+
)
152+
spacegroups = self.inputs["structures_config"].get(
153+
f"{self.structure_name}_spacegroup", None
154+
)
155+
spacegroups = None if spacegroups == ["auto"] else spacegroups
156+
self.pdfadapter.initialize_structures(
157+
[self.inputs["structure_path"]],
158+
run_parallel=True,
159+
spacegroups=spacegroups,
160+
names=[self.structure_name],
161+
)
162+
self.pdfadapter.initialize_contribution(self.inputs["equation"][0])
163+
self.pdfadapter.initialize_recipe()
164+
for i in range(len(self.inputs["equation_variable"])):
165+
self.pdfadapter.recipe.addVar(
166+
getattr(
167+
list(self.pdfadapter.recipe._contributions.values())[0],
168+
self.inputs["equation_variable"][i],
169+
)
170+
)
171+
self.pdfadapter.set_initial_variable_values(
172+
self.inputs["initial_values"]
173+
)
174+
175+
def run(self):
176+
self.pdfadapter.recipe.fix("all")
177+
for var in self.inputs["variables"]:
178+
self.pdfadapter.recipe.free(var)
179+
least_squares(
180+
self.pdfadapter.recipe.residual, self.pdfadapter.recipe.values
181+
)
182+
if "result_path" in self.inputs:
183+
with open(self.inputs["result_path"], "w") as f:
184+
json.dump(self.pdfadapter.get_results(), f, indent=4)
185+
return self.pdfadapter.get_results()
186+
187+
def run_app(self, args):
188+
dpin_path = Path(args.input_file)
189+
if not dpin_path.exists():
190+
raise FileNotFoundError(
191+
f"{str(dpin_path)} not found. Please check if this file "
192+
"exists and provide the correct path to it."
193+
)
194+
dsl_code = dpin_path.read_text()
195+
self.interpret(dsl_code)
196+
self.configure_adapter()
197+
self.run()
198+
199+
200+
if __name__ == "__main__":
201+
interpreter = DiffpyInterpreter()
202+
code = f"""
203+
load structure G1 from "{str(Path(__file__).parents[3] / "tests/data/Ni.cif")}"
204+
load profile exp_ni from "{str(Path(__file__).parents[3] / "tests/data/Ni.gr")}"
205+
206+
set G1 spacegroup as auto
207+
set exp_ni q_range as 0.1 25
208+
set exp_ni calculation_range as 1.5 50 0.01
209+
create equation variables s0
210+
set equation as "s0*G1"
211+
save to "results.json"
212+
213+
variables:
214+
---
215+
- G1.a: 3.52
216+
- s0: 0.4
217+
- G1.Uiso_0: 0.005
218+
- G1.delta2: 2
219+
- qdamp: 0.04
220+
- qbroad: 0.02
221+
---
222+
""" # noqa: E501
223+
interpreter.interpret(code)
224+
interpreter.configure_adapter()
225+
recipe = interpreter.pdfadapter.recipe
226+
for pname, param in recipe._parameters.items():
227+
print(f"{pname}: {param.value}")
228+
print(interpreter.inputs["variables"])

0 commit comments

Comments
 (0)