11import logging
22import re
3+ from pathlib import Path
34
45import networkx as nx
6+ from pyobjcryst import loadCrystal
57
68from diffpy .srfit .fitbase import FitContribution
79from diffpy .srfit .fitbase .parameter import Parameter , ParameterProxy
810from diffpy .srfit .pdf .pdfgenerator import PDFGenerator
911from diffpy .srfit .structure import constrain_as_space_group
10- from diffpy .structure import Structure
12+ from diffpy .srfit .structure .diffpyparset import DiffpyStructureParSet
13+ from diffpy .srfit .structure .objcrystparset import ObjCrystCrystalParSet
14+ from diffpy .structure .parsers import get_parser
1115
1216# NOTE: MCP server prefers logging for output
1317logger = logging .getLogger (__name__ )
@@ -152,9 +156,13 @@ def residual(self):
152156
153157
154158class ParametricModelEquation (ParametricModel ):
155- def __init__ (self , name , equation_str = None ):
159+ def __init__ (self , name , equation_str = None , from_model_name = None ):
156160 super ().__init__ (name = name )
157161 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 )
158166 if equation_str :
159167 self .set_equation (equation_str )
160168
@@ -170,14 +178,6 @@ def set_equation(self, equation_str):
170178 def get_equation (self ):
171179 return self .equation_str
172180
173- def set_residual_equation (self , residual_equation_str ):
174- self .residual_equation = residual_equation_str
175- self .calc_obj .set_residual_equation (residual_equation_str )
176- self ._rebuild_graph ()
177-
178- def get_residual_equation (self ):
179- return self .residual_equation
180-
181181 def evaluate (self ):
182182 yc = self .calc_obj ._eq ()
183183 if (
@@ -194,60 +194,171 @@ def residual(self):
194194class ParametricModelPDF (ParametricModel ):
195195 # NOTE: qmin, qmax, stype(scattering type) are meta handled
196196 # throughout the loaded profile in the refinement session
197- def __init__ (self , name , structure : Structure ):
197+ def __init__ (
198+ self ,
199+ name ,
200+ structure_file_path = None ,
201+ from_model_name = None ,
202+ ):
198203 super ().__init__ (name = name )
199204 self .calc_obj = PDFGenerator (name )
200- self .calc_obj .setStructure (structure )
205+ # NOTE: Certain space groups require dual origin handling.
206+ DUAL_ORIGIN_SG_NUMBERS = {
207+ 48 ,
208+ 50 ,
209+ 59 ,
210+ 68 ,
211+ 70 ,
212+ 85 ,
213+ 86 ,
214+ 88 ,
215+ 125 ,
216+ 126 ,
217+ 129 ,
218+ 130 ,
219+ 133 ,
220+ 134 ,
221+ 137 ,
222+ 138 ,
223+ 141 ,
224+ 142 ,
225+ 201 ,
226+ 203 ,
227+ 222 ,
228+ 224 ,
229+ 227 ,
230+ 228 ,
231+ }
232+ if structure_file_path is not None :
233+ stru_parser = get_parser ("auto" )
234+ structure = stru_parser .parse (
235+ Path (structure_file_path ).read_text ()
236+ )
237+ sg = getattr (stru_parser , "spacegroup" , None )
238+ self .space_group_symbol = sg .short_name if sg is not None else "P1"
239+ if sg .number in DUAL_ORIGIN_SG_NUMBERS :
240+ structure = loadCrystal (structure_file_path )
241+ self .calc_obj .setStructure (structure )
242+ else :
243+ self .calc_obj .setStructure (structure )
244+ elif from_model_name is not None :
245+ self .calc_obj .setPhase (from_model_name .calc_obj .phase )
246+ self .space_group_symbol = from_model_name .space_group_symbol
247+ else :
248+ raise ValueError (
249+ "Either structure_file or from_model must be provided."
250+ )
251+ self .sgpar_names = []
201252 self ._rebuild_graph ()
202- self ._hide_dependent_parameters ()
203253
204- def _hide_dependent_parameters (self ):
205- dependent_par_names = [
206- r"\.U21$" ,
207- r"\.U31$" ,
208- r"\.U32$" , # U21=U12, U31=U13, U32=U23
209- r"\.Biso" ,
210- r"\.B\d{2}" , # Bij = Uij * 8 * pi^2
211- r"\.occupancy$" , # occupancy=oc
212- ]
254+ def _hide_dependent_parameters (self , use_uiso = True ):
255+ if use_uiso :
256+ dependent_par_names = [
257+ r"\.U21$" ,
258+ r"\.U31$" ,
259+ r"\.U32$" , # U21=U12, U31=U13, U32=U23
260+ r"\.Biso" ,
261+ r"\.B\d{2}" , # Bij = Uij * 8 * pi^2
262+ r"\.occupancy$" , # occupancy=oc
263+ ]
264+ else :
265+ dependent_par_names = [
266+ r"\.B21$" ,
267+ r"\.B31$" ,
268+ r"\.B32$" ,
269+ r"\.Uiso" ,
270+ r"\.U\d{2}" ,
271+ r"\.occupancy$" , # occupancy=oc
272+ ]
213273 regex = re .compile ("|" .join (dependent_par_names ))
214274 for par_name in self .parameters .keys ():
215275 if regex .search (par_name ):
216276 self ._graph .nodes [par_name ]["constrained_or_constant" ] = True
217277
218- def constrain_symmetry (self , spacegroup_symbol ):
219- space_group_parset = constrain_as_space_group (
220- self .calc_obj .phase , spacegroup_symbol
221- )
278+ def constrain_symmetry (self , spacegroup_symbol = None , use_uiso = True ):
279+ if spacegroup_symbol is None :
280+ spacegroup_symbol = self .space_group_symbol
281+ if isinstance (self .calc_obj .phase , DiffpyStructureParSet ):
282+ space_group_parset = constrain_as_space_group (
283+ self .calc_obj .phase , spacegroup_symbol
284+ )
285+ self ._hide_dependent_parameters (use_uiso = use_uiso )
286+ elif isinstance (self .calc_obj .phase , ObjCrystCrystalParSet ):
287+ if use_uiso is not False :
288+ use_uiso = False
289+ logger .warning (
290+ "ObjCrystCrystalParSet prefers using the letter B instead "
291+ "U for ADP parameters."
292+ )
293+ self ._hide_dependent_parameters (use_uiso = use_uiso )
294+ else :
295+ raise ValueError (
296+ "Unsupported calculation object type."
297+ "Currently supported types are "
298+ "DiffpyStructureParSet and ObjCrystCrystalParSet."
299+ )
222300 # hide constrained parameters in the graph
223- symmetry_par_names = [
224- r"\.a$" ,
225- r"\.b$" ,
226- r"\.c$" ,
227- r"\.alpha$" ,
228- r"\.beta$" ,
229- r"\.gamma$" ,
230- r"\.x$" ,
231- r"\.y$" ,
232- r"\.z$" ,
233- r"\.Uiso$" ,
234- r"\.U11$" ,
235- r"\.U22$" ,
236- r"\.U33$" ,
237- r"\.U12$" ,
238- r"\.U13$" ,
239- r"\.U23$" ,
240- ]
301+ if use_uiso :
302+ symmetry_par_names = [
303+ r"\.a$" ,
304+ r"\.b$" ,
305+ r"\.c$" ,
306+ r"\.alpha$" ,
307+ r"\.beta$" ,
308+ r"\.gamma$" ,
309+ r"\.x$" ,
310+ r"\.y$" ,
311+ r"\.z$" ,
312+ r"\.Uiso$" ,
313+ r"\.U11$" ,
314+ r"\.U22$" ,
315+ r"\.U33$" ,
316+ r"\.U12$" ,
317+ r"\.U13$" ,
318+ r"\.U23$" ,
319+ ]
320+ else :
321+ symmetry_par_names = [
322+ r"\.a$" ,
323+ r"\.b$" ,
324+ r"\.c$" ,
325+ r"\.alpha$" ,
326+ r"\.beta$" ,
327+ r"\.gamma$" ,
328+ r"\.x$" ,
329+ r"\.y$" ,
330+ r"\.z$" ,
331+ r"\.Biso$" ,
332+ r"\.B11$" ,
333+ r"\.B22$" ,
334+ r"\.B33$" ,
335+ r"\.B12$" ,
336+ r"\.B13$" ,
337+ r"\.B23$" ,
338+ ]
241339 free_variables = []
242- for latpar in space_group_parset .latpars :
243- free_variables .append (latpar )
244- for adpar in space_group_parset .adppars :
245- free_variables .append (adpar )
246- for xyzpar in space_group_parset .xyzpars :
247- free_variables .append (xyzpar )
340+ if isinstance (self .calc_obj .phase , DiffpyStructureParSet ):
341+ for latpar in space_group_parset .latpars :
342+ free_variables .append (latpar )
343+ for adpar in space_group_parset .adppars :
344+ free_variables .append (adpar )
345+ for xyzpar in space_group_parset .xyzpars :
346+ free_variables .append (xyzpar )
347+ elif isinstance (self .calc_obj .phase , ObjCrystCrystalParSet ):
348+ for par in self .calc_obj .phase .sgpars :
349+ free_variables .append (par )
248350 for i in range (len (free_variables )):
249351 while isinstance (free_variables [i ], ParameterProxy ):
250352 free_variables [i ] = free_variables [i ].par
353+ organized_name = "." .join (
354+ [
355+ obj .name
356+ for obj in self .calc_obj ._locate_managed_object (
357+ free_variables [i ]
358+ )
359+ ]
360+ )
361+ self .sgpar_names .append (organized_name )
251362 symmetry_par_regex = re .compile ("|" .join (symmetry_par_names ))
252363 for par_name , par in self .parameters .items ():
253364 if symmetry_par_regex .search (par_name ):
0 commit comments