Skip to content

Commit 379895d

Browse files
committed
feat: support nanoparticle pdf refinement
1 parent 484487e commit 379895d

9 files changed

Lines changed: 4105 additions & 73 deletions

File tree

news/nanoparticle.rst

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
**Added:**
2+
3+
* Added support for nanoparticle pdf refinement.
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>

src/diffpy/apps/refinebase/parametric_model.py

Lines changed: 97 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,8 @@
2020
class ParametricModel:
2121
def __init__(self, name):
2222
self.name = name
23-
self.calc_obj = FitContribution(name)
23+
self._contribution = FitContribution(name)
24+
self.calc_obj = self._contribution
2425
self._graph = nx.DiGraph()
2526
# all submodels will share the same profile
2627
self._submodels = []
@@ -58,8 +59,7 @@ def _construct_parameter_graph(
5859
)
5960
self._graph.add_edge(parent_name, child_name)
6061
self._construct_parameter_graph(
61-
obj,
62-
prefix=f"{parent_name}.",
62+
obj, prefix=f"{parent_name}.", old_graph=old_graph
6363
)
6464

6565
def register_submodel(self, submodel, symbol=None):
@@ -70,34 +70,47 @@ def register_submodel(self, submodel, symbol=None):
7070
)
7171
if symbol is None:
7272
symbol = submodel.name
73-
if symbol in self.calc_obj._parameters:
74-
self.calc_obj._remove_parameter(self.calc_obj._parameters[symbol])
73+
if symbol in self._contribution._parameters:
74+
self._contribution._remove_parameter(
75+
self._contribution._parameters[symbol]
76+
)
7577
if isinstance(submodel, ParametricModelPDF):
7678
if symbol != submodel.name:
7779
logger.warning(
7880
f"ParametricModelPDF's name ({submodel.name}) does "
7981
f"not match with the provided symbol ({symbol}) ",
8082
)
81-
self.calc_obj.add_profile_generator(submodel.calc_obj)
82-
elif isinstance(submodel, ParametricModelEquation):
83-
self.calc_obj._eqfactory.registerOperator(
84-
symbol, submodel.calc_obj._eq
83+
self._contribution.add_profile_generator(submodel.calc_obj)
84+
elif isinstance(submodel, ParametricModelEquation) or isinstance(
85+
submodel, ParametricModelFunction
86+
):
87+
self._contribution._eqfactory.registerOperator(
88+
symbol, submodel._contribution._eq
8589
)
86-
self.calc_obj.add_parameter_set(submodel.calc_obj)
90+
self._contribution.add_parameter_set(submodel._contribution)
8791
else:
8892
raise NotImplementedError(
89-
"Only ParametricModelPDF and ParametricModelEquation "
93+
"Only ParametricModelPDF, ParametricModelEquation, "
94+
"and ParametricModelFunction "
9095
"instances are supported to be registered as submodels."
9196
)
9297
if self.equation_str is not None:
93-
self.calc_obj.set_equation(self.equation_str)
98+
self._contribution.set_equation(self.equation_str)
99+
submodel._rebuild_graph()
100+
if f"{self.name}.{submodel.name}" not in self._graph.nodes:
101+
self._graph.add_node(
102+
symbol,
103+
parameter=None,
104+
constrained_or_constant=False,
105+
)
106+
self._graph.add_edge(self.name, symbol)
107+
subgraph = submodel._graph.copy()
108+
mapping = {node: f"{self.name}.{node}" for node in subgraph.nodes}
109+
subgraph = nx.relabel_nodes(subgraph, mapping)
110+
self._graph = nx.compose(self._graph, subgraph)
94111
self._submodels.append(submodel)
95112
self._rebuild_graph()
96113

97-
def process_meta_data(self, meta):
98-
if hasattr(self.calc_obj, "process_meta_data"):
99-
self.calc_obj.process_meta_data(meta)
100-
101114
@property
102115
def parameters(self):
103116
return {
@@ -131,17 +144,17 @@ def independent_parameters(self):
131144
}
132145

133146
def set_profile(self, profile):
134-
self.calc_obj.set_profile(profile)
147+
self._contribution.set_profile(profile)
135148
for submodel in self._submodels:
136149
if hasattr(submodel, "set_profile"):
137150
submodel.set_profile(profile)
138151
self._rebuild_graph()
139152

140153
def _rebuild_graph(self):
141154
old_graph = self._graph
142-
self._graph.clear()
155+
self._graph = nx.DiGraph()
143156
self._construct_parameter_graph(
144-
self.calc_obj, prefix="", old_graph=old_graph
157+
self._contribution, prefix="", old_graph=old_graph
145158
)
146159

147160
def evaluate(self):
@@ -156,39 +169,77 @@ def residual(self):
156169

157170

158171
class ParametricModelEquation(ParametricModel):
159-
def __init__(self, name, equation_str=None, from_model_name=None):
172+
def __init__(self, name, equation_str=None):
160173
super().__init__(name=name)
161174
self.equation_str = None
162-
if from_model_name is not None:
163-
for name, obj in from_model_name.calc_obj.__dict__.items():
164-
if name not in ["name", "profile", "_observers"]:
165-
setattr(self.calc_obj, name, obj)
166175
if equation_str:
167176
self.set_equation(equation_str)
168177

169-
@property
170-
def _contribution(self):
171-
return self.calc_obj
172-
173178
def set_equation(self, equation_str):
174179
self.equation_str = equation_str
175-
self.calc_obj.set_equation(equation_str)
180+
self._contribution.set_equation(equation_str)
176181
self._rebuild_graph()
177182

178183
def get_equation(self):
179184
return self.equation_str
180185

181186
def evaluate(self):
182-
yc = self.calc_obj._eq()
187+
yc = self._contribution._eq()
183188
if (
184-
hasattr(self.calc_obj, "profile")
185-
and self.calc_obj.profile is not None
189+
hasattr(self._contribution, "profile")
190+
and self._contribution.profile is not None
186191
):
187-
self.calc_obj.profile.ycalc = yc
192+
self._contribution.profile.ycalc = yc
188193
return yc
189194

190195
def residual(self):
191-
return self.calc_obj.residual()
196+
return self._contribution.residual()
197+
198+
199+
class ParametricModelFunction(ParametricModel):
200+
def __init__(self, name, function, argnames=None):
201+
"""
202+
Initialize a ParametricModelFunction instance.
203+
204+
function can be either a callable or a string representing
205+
the pre-defined function.
206+
One and only one of func or characteristic_func_name must be provided.
207+
Allowed value for characteristic_func_name:
208+
"spherical_particle",
209+
"spheroidal_particle",
210+
"lognormal_spherical_particle",
211+
"sheet_particle",
212+
"shell_particle",
213+
"SASCF",
214+
"sphericalCF",
215+
"spheroidalCF",
216+
"spheroidalCF2",
217+
"lognormalSphericalCF",
218+
"sheetCF",
219+
"shellCF",
220+
"shellCF2",
221+
"""
222+
super().__init__(name=name)
223+
if isinstance(function, str):
224+
import diffpy.srfit.pdf.characteristicfunctions
225+
226+
function = getattr(
227+
diffpy.srfit.pdf.characteristicfunctions,
228+
function,
229+
)
230+
self._contribution.register_function(function, argnames=argnames)
231+
self._contribution.set_equation(function.__name__)
232+
self.calc_obj = function
233+
self._rebuild_graph()
234+
235+
def set_profile(self, profile, xname=None, yname=None, dyname=None):
236+
self._contribution.set_profile(
237+
profile, xname=xname, yname=yname, dyname=dyname
238+
)
239+
# no submodel is allowed ParametricModelFunction
240+
241+
def evaluate(self):
242+
return self._contribution._eq()
192243

193244

194245
class ParametricModelPDF(ParametricModel):
@@ -199,6 +250,7 @@ def __init__(
199250
name,
200251
structure_file_path=None,
201252
from_model_name=None,
253+
library="Diffpy",
202254
):
203255
super().__init__(name=name)
204256
self.calc_obj = PDFGenerator(name)
@@ -236,7 +288,7 @@ def __init__(
236288
)
237289
sg = getattr(stru_parser, "spacegroup", None)
238290
self.space_group_symbol = sg.short_name if sg is not None else "P1"
239-
if sg.number in DUAL_ORIGIN_SG_NUMBERS:
291+
if sg.number in DUAL_ORIGIN_SG_NUMBERS or library == "ObjCryst":
240292
structure = loadCrystal(structure_file_path)
241293
self.calc_obj.setStructure(structure)
242294
else:
@@ -369,10 +421,18 @@ def constrain_symmetry(self, spacegroup_symbol=None, use_uiso=True):
369421
"constrained_or_constant"
370422
] = True
371423

424+
def _rebuild_graph(self):
425+
old_graph = self._graph
426+
self._graph = nx.DiGraph()
427+
self._construct_parameter_graph(
428+
# PDFGenerator itself holds parameters
429+
self.calc_obj,
430+
prefix="",
431+
old_graph=old_graph,
432+
)
433+
372434
def set_profile(self, profile):
373435
self.calc_obj.set_profile(profile)
374-
self._yname = self.calc_obj.profile.ypar.name
375-
self._dyname = self.calc_obj.profile.dypar.name
376436
# no submodel is allowed ParametricModelPDF
377437

378438
def evaluate(self):

src/diffpy/apps/refinebase/refinement_playbook.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -112,4 +112,4 @@ Notes specific to this case: `pdf_ni`/`pdf_ni_neutron`/`pdf_ni_partial` share on
112112
## Unverified — check before relying on
113113

114114
- Exact pairing of `weights`/`restraints`/`metas` in `solve`.
115-
- Whether the live connected `add_pdf_model` accepts `structure_lib` ("Diffpy"/"PyObjcryst") — seen on one live schema fetch but absent from the server source reviewed here.
115+
- Whether the live connected `add_pdf_model` accepts `structure_lib` ("Diffpy"/"PyObjcryst") — seen on one live schema fetch but absent from the server source reviewed here.

0 commit comments

Comments
 (0)