Skip to content

Commit c555166

Browse files
committed
feat: add ParametricModelPDF symmetry constraints
1 parent 30c3040 commit c555166

9 files changed

Lines changed: 244 additions & 218 deletions

File tree

‎requirements/pip.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
numpy
1+
numpy

‎requirements/tests.txt‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ pytest
33
codecov
44
coverage
55
pytest-cov
6-
pytest-env
6+
pytest-env
Lines changed: 87 additions & 122 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
11
import re
2-
from collections import OrderedDict
32

43
import networkx as nx
54

6-
from diffpy.srfit.equation.literals import Operator, makeOperator
75
from diffpy.srfit.fitbase import FitContribution
86
from diffpy.srfit.fitbase.parameter import Parameter, ParameterProxy
97
from diffpy.srfit.pdf.pdfgenerator import PDFGenerator
@@ -14,10 +12,8 @@
1412
class ParametricModel:
1513
def __init__(self, name):
1614
self.name = name
17-
self.description = ""
18-
self._contribution = FitContribution(name)
15+
self.calc_obj = FitContribution(name)
1916
self._graph = nx.DiGraph()
20-
self.meta = OrderedDict()
2117
# all submodels will share the same profile
2218
self._submodels = []
2319

@@ -26,9 +22,7 @@ def _construct_parameter_graph(
2622
):
2723
parent_name = f"{prefix}{parameterset.name}"
2824
self._graph.add_node(parent_name, parameter=parameterset)
29-
for par in parameterset._iter_local_parameters(
30-
regexp=re.compile("") # regex is a required argument
31-
):
25+
for par in parameterset._iter_local_parameters(regexp=re.compile("")):
3226
par_node_id = f"{parent_name}.{par.name}"
3327
if not old_graph or par_node_id not in old_graph.nodes:
3428
self._graph.add_node(
@@ -37,8 +31,6 @@ def _construct_parameter_graph(
3731
constrained_or_constant=False,
3832
)
3933
else:
40-
# apply stored constrained_or_constant information
41-
# from the old graph
4234
self._graph.add_node(
4335
par_node_id,
4436
parameter=par,
@@ -47,7 +39,6 @@ def _construct_parameter_graph(
4739
],
4840
)
4941
self._graph.add_edge(parent_name, par_node_id)
50-
5142
for obj in parameterset._iter_managed_parameter_containers():
5243
if hasattr(obj, "_iter_managed_parameter_containers"):
5344
child_name = f"{parent_name}.{obj.name}"
@@ -63,29 +54,37 @@ def _construct_parameter_graph(
6354
prefix=f"{parent_name}.",
6455
)
6556

66-
def register_submodel(self, symbol, model):
67-
"""Register a parametric model to the current model."""
68-
op = model.evaluate
69-
if not isinstance(model.evaluate, Operator):
70-
op = makeOperator(
71-
name=model.name,
72-
symbol=symbol,
73-
operation=model.evaluate,
74-
nin=0, # NOTE: make this (nin, nout) work for all scenarios
75-
nout=1,
57+
def register_submodel(self, submodel, symbol=None):
58+
if not isinstance(self, ParametricModelEquation):
59+
raise ValueError(
60+
"Submodels can only be registered to "
61+
"ParametricModelEquation instance."
62+
)
63+
if symbol is None:
64+
symbol = submodel.name
65+
if symbol in self.calc_obj._parameters:
66+
self.calc_obj._remove_parameter(self.calc_obj._parameters[symbol])
67+
if isinstance(submodel, ParametricModelPDF):
68+
self.calc_obj.add_profile_generator(submodel.calc_obj)
69+
elif isinstance(submodel, ParametricModelEquation):
70+
self.calc_obj._eqfactory.registerOperator(
71+
symbol, submodel.calc_obj._eq
7672
)
77-
# NOTE: registerOperator -> register_operator
78-
# once updated in diffpy.srfit.
79-
self._contribution._eqfactory.registerOperator(symbol, op)
80-
# Replace parameter with the submodel's parameterset
81-
if symbol in self._contribution._parameters:
82-
self._contribution._remove_parameter(
83-
self._contribution._parameters[symbol]
73+
self.calc_obj.add_parameter_set(submodel.calc_obj)
74+
else:
75+
raise NotImplementedError(
76+
"Only ParametricModelPDF and ParametricModelEquation "
77+
"instances are supported to be registered as submodels."
8478
)
85-
self._contribution.add_parameter_set(model._contribution)
86-
self._submodels.append(model)
79+
if self.equation_str is not None:
80+
self.calc_obj.set_equation(self.equation_str)
81+
self._submodels.append(submodel)
8782
self._rebuild_graph()
8883

84+
def process_meta_data(self, meta):
85+
if hasattr(self.calc_obj, "process_meta_data"):
86+
self.calc_obj.process_meta_data(meta)
87+
8988
@property
9089
def parameters(self):
9190
return {
@@ -119,114 +118,70 @@ def independent_parameters(self):
119118
}
120119

121120
def set_profile(self, profile):
122-
self._contribution.set_profile(profile)
123-
if hasattr(self, "pdf_generator"):
124-
self.pdf_generator.set_profile(profile)
121+
self.calc_obj.set_profile(profile)
125122
for submodel in self._submodels:
126-
submodel.set_profile(profile)
123+
if hasattr(submodel, "set_profile"):
124+
submodel.set_profile(profile)
125+
self._rebuild_graph()
127126

128127
def _rebuild_graph(self):
129-
"""
130-
Rebuild the parameter graph based on the current contribution.
131-
"""
132128
old_graph = self._graph
133129
self._graph.clear()
134130
self._construct_parameter_graph(
135-
self._contribution, prefix="", old_graph=old_graph
131+
self.calc_obj, prefix="", old_graph=old_graph
136132
)
137133

138134
def evaluate(self):
139-
return self._contribution._eq()
135+
return self.calc_obj._eq()
140136

141137

142138
class ParametricModelEquation(ParametricModel):
143-
def __init__(self, name, equation_str):
139+
def __init__(self, name, equation_str=None):
144140
super().__init__(name=name)
145-
self._contribution.set_equation(equation_str)
146-
self.description = f"ParametricModelEq: {equation_str}"
141+
self.equation_str = None
142+
if equation_str:
143+
self.equation_str = equation_str
144+
self.calc_obj.set_equation(equation_str)
145+
self._rebuild_graph()
146+
147+
@property
148+
def _contribution(self):
149+
return self.calc_obj
150+
151+
def set_equation(self, equation_str):
152+
self.equation_str = equation_str
153+
self.calc_obj.set_equation(equation_str)
147154
self._rebuild_graph()
148155

149156

150157
class ParametricModelPDF(ParametricModel):
151-
def __init__(self, name, structure: Structure, meta=None):
158+
def __init__(self, name, structure: Structure):
152159
super().__init__(name=name)
153-
self.description = f"ParametricModelPDF: {name}"
154-
self._initialize_pdf_generator(structure, meta)
160+
self.calc_obj = PDFGenerator(name)
161+
self.calc_obj.setStructure(structure)
155162
self._rebuild_graph()
156-
# Constrain aliased parameters
157-
constrained_par_names = [
158-
r"\.U21$", # U21=U12
163+
self._hide_dependent_parameters()
164+
165+
def _hide_dependent_parameters(self):
166+
dependent_par_names = [
167+
r"\.U21$",
159168
r"\.U31$",
160-
r"\.U32$",
161-
r"\.B21$",
162-
r"\.B31$",
163-
r"\.B32$",
169+
r"\.U32$", # U21=U12, U31=U13, U32=U23
170+
r"\.Biso",
171+
r"\.B\d{2}", # Bij = Uij * 8 * pi^2
164172
r"\.occupancy$", # occupancy=oc
165173
]
166-
constrained_par_regex = re.compile("|".join(constrained_par_names))
174+
regex = re.compile("|".join(dependent_par_names))
167175
for par_name in self.parameters.keys():
168-
if constrained_par_regex.search(par_name):
176+
if regex.search(par_name):
169177
self._graph.nodes[par_name]["constrained_or_constant"] = True
170178

171-
if meta is not None:
172-
self.process_metadata(meta)
173-
174-
# Add a placeholder equation to pass the validation
175-
old_validate = self._contribution._validate
176-
self._contribution._validate = lambda: self.validate(old_validate)
177-
178-
def validate(self, old_validate):
179-
if self._contribution._eq is None:
180-
placeholder_equation = makeOperator(
181-
name=self.pdf_generator.name,
182-
symbol=f"{self.name}_eq_placeholder",
183-
operation=self.pdf_generator.operation,
184-
nin=0,
185-
nout=1,
186-
)
187-
self._contribution.set_equation(
188-
"g", ns={"g": placeholder_equation}
189-
)
190-
old_validate()
191-
192-
def _initialize_pdf_generator(self, structure: Structure, meta: dict):
193-
self.pdf_generator = PDFGenerator(self.name)
194-
# NOTE: setStructure -> set_structure
195-
# once updated in diffpy.srfit.
196-
self.pdf_generator.setStructure(structure)
197-
# Get access to all parameters in the PDFGenerator
198-
self._contribution.add_parameter_set(self.pdf_generator)
199-
self._contribution._RecipeContainer__managed = (
200-
self.pdf_generator._RecipeContainer__managed
201-
)
202-
self._contribution._parameters = self.pdf_generator._parameters
203-
204-
def process_metadata(self, meta: dict = None):
205-
self.pdf_generator.meta.update(meta)
206-
self.pdf_generator._process_metadata()
207-
208-
def evaluate(self):
209-
if self._contribution.profile is None:
210-
raise ValueError("Profile is not set for the PDF model.")
211-
return self.pdf_generator.operation()
212-
213-
def constrain_symmetry(self, space_group: str):
179+
def constrain_symmetry(self, spacegroup_symbol):
214180
space_group_parset = constrain_as_space_group(
215-
self.pdf_generator.phase, space_group
181+
self.calc_obj.phase, spacegroup_symbol
216182
)
217-
# Get the independent parameters from spacegroup symmetry
218-
free_variables = []
219-
for latpar in space_group_parset.latpars:
220-
free_variables.append(latpar)
221-
for adpar in space_group_parset.adppars:
222-
free_variables.append(adpar)
223-
for xyzpar in space_group_parset.xyzpars:
224-
free_variables.append(xyzpar)
225-
for i in range(len(free_variables)):
226-
while isinstance(free_variables[i], ParameterProxy):
227-
free_variables[i] = free_variables[i].par
228-
# Add constraints information for dependent parameters
229-
symmetry_parnames = [
183+
# hide constrained parameters in the graph
184+
symmetry_par_names = [
230185
r"\.a$",
231186
r"\.b$",
232187
r"\.c$",
@@ -243,22 +198,32 @@ def constrain_symmetry(self, space_group: str):
243198
r"\.U12$",
244199
r"\.U13$",
245200
r"\.U23$",
246-
r"\.Biso$",
247-
r"\.B11$",
248-
r"\.B22$",
249-
r"\.B33$",
250-
r"\.B12$",
251-
r"\.B13$",
252-
r"\.B23$",
253201
]
254-
symmetry_par_regex = re.compile("|".join(symmetry_parnames))
202+
free_variables = []
203+
for latpar in space_group_parset.latpars:
204+
free_variables.append(latpar)
205+
for adpar in space_group_parset.adppars:
206+
free_variables.append(adpar)
207+
for xyzpar in space_group_parset.xyzpars:
208+
free_variables.append(xyzpar)
209+
for i in range(len(free_variables)):
210+
while isinstance(free_variables[i], ParameterProxy):
211+
free_variables[i] = free_variables[i].par
212+
symmetry_par_regex = re.compile("|".join(symmetry_par_names))
255213
for par_name, par in self.parameters.items():
256-
if par.constrained or par.const:
257-
self._graph.nodes[par_name]["constrained_or_constant"] = True
258-
elif symmetry_par_regex.search(par_name):
214+
if symmetry_par_regex.search(par_name):
259215
while isinstance(par, ParameterProxy):
260216
par = par.par
261217
if par not in free_variables:
262218
self._graph.nodes[par_name][
263219
"constrained_or_constant"
264220
] = True
221+
222+
def set_qmin(self, qmin: float):
223+
self.calc_obj.setQmin(qmin)
224+
225+
def set_qmax(self, qmax: float):
226+
self.calc_obj.setQmax(qmax)
227+
228+
def set_scattering_type(self, scattering_type: str):
229+
self.calc_obj.setScatteringType(scattering_type)

‎src/diffpy/apps/refinebase/refinement_server.py‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -234,7 +234,7 @@ async def refine(
234234
]
235235

236236
# Perform refinement
237-
session.solve(
237+
session._solve(
238238
profile_objs,
239239
model_objs,
240240
variable_objs,

‎src/diffpy/apps/refinebase/refinement_session.py‎

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,9 @@
22
from collections import OrderedDict
33

44
import numpy
5-
from scipy.optimize import leastsq
5+
from scipy.optimize import least_squares
66

7-
from diffpy.apps.refinebase.parametric_model import (
8-
ParametricModel,
9-
)
7+
from diffpy.apps.refinebase.parametric_model import ParametricModel
108
from diffpy.srfit.fitbase import (
119
FitRecipe,
1210
Profile,
@@ -52,7 +50,7 @@ def get_variable(self, variable_name):
5250
)
5351
return self.models_dict[objs[0]].parameters[variable_name]
5452

55-
def solve(
53+
def _solve(
5654
self,
5755
profiles,
5856
models,
@@ -73,10 +71,15 @@ def solve(
7371
if initial_values is not None:
7472
for var, val in zip(variables, initial_values):
7573
var.value = val
74+
if len(set([var.name for var in variables])) != len(variables):
75+
raise ValueError(
76+
"Duplicate variable names found. Please ensure that "
77+
"each variable to be refined has a unique name."
78+
)
7679
for var in variables:
7780
recipe.add_variable(var)
7881
# Refine the recipe
7982
recipe.fix("all")
8083
for i in range(len(variables)):
8184
recipe.free(variables[i].name)
82-
leastsq(recipe.residual, recipe.getValues())
85+
least_squares(recipe.residual, recipe.getValues(), x_scale="jac")

0 commit comments

Comments
 (0)