Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .codespell/ignore_words.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,6 @@ mater

;; Frobenius norm used in np.linalg.norm
fro

;; "number of input arguments" used in diffpy.srfit.equation.literals.Operator
nin
23 changes: 23 additions & 0 deletions news/refinement.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* Add more flexible interface to utilize ``diffpy.srfit``.

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
2 changes: 2 additions & 0 deletions requirements/conda.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,5 @@ pyyaml
diffpy.srfit
diffpy.srreal
diffpy.structure
networkx
mcp[cli]
14 changes: 0 additions & 14 deletions src/diffpy/__init__.py
Original file line number Diff line number Diff line change
@@ -1,14 +0,0 @@
#!/usr/bin/env python
##############################################################################
#
# (c) 2026 The Trustees of Columbia University in the City of New York.
# All rights reserved.
#
# File coded by: Billinge Group members and community contributors.
#
# See GitHub contributions for a more detailed list of contributors.
# https://github.com/diffpy/diffpy.apps/graphs/contributors
#
# See LICENSE.rst for license information.
#
##############################################################################
34 changes: 17 additions & 17 deletions src/diffpy/apps/pdfadapter.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ def initialize_profile(
"""
profile = Profile()
parser = PDFParser()
parser.parseString(Path(profile_path).read_text())
profile.loadParsedData(parser)
parser.parse_file(profile_path)
profile.load_parsed_data(parser)
if q_range is not None:
profile.meta["qmin"] = q_range[0]
profile.meta["qmax"] = q_range[1]
Expand All @@ -77,7 +77,7 @@ def initialize_profile(
"xmax": calculation_range[1],
"dx": calculation_range[2],
}
profile.setCalculationRange(**calculation_range)
profile.set_calculation_range(**calculation_range)
self.profile = profile

def initialize_structures(
Expand Down Expand Up @@ -185,10 +185,10 @@ def initialize_contribution(self, equation=None):
"""
equation = equation[0] if equation is not None else None
contribution = FitContribution("pdfcontribution")
contribution.setProfile(self.profile)
contribution.set_profile(self.profile)
for pdfgenerator in self.pdfgenerators:
contribution.addProfileGenerator(pdfgenerator)
contribution.setEquation(equation)
contribution.add_profile_generator(pdfgenerator)
contribution.set_equation(equation)
self.contribution = contribution
return self.contribution

Expand All @@ -204,9 +204,9 @@ def initialize_recipe(
"""

recipe = FitRecipe()
recipe.addContribution(self.contribution)
qdamp = recipe.newVar("qdamp", fixed=False, value=0.04)
qbroad = recipe.newVar("qbroad", fixed=False, value=0.02)
recipe.add_contribution(self.contribution)
qdamp = recipe.create_new_variable("qdamp", fixed=False, value=0.04)
qbroad = recipe.create_new_variable("qbroad", fixed=False, value=0.02)
for i, (pdfgenerator, spacegroup) in enumerate(
zip(self.pdfgenerators, self.spacegroups)
):
Expand All @@ -215,23 +215,23 @@ def initialize_recipe(
"delta2",
]:
par = getattr(pdfgenerator, pname)
recipe.addVar(
recipe.add_variable(
par, name=f"{pdfgenerator.name}_{pname}", fixed=False
)
recipe.constrain(pdfgenerator.qdamp, qdamp)
recipe.constrain(pdfgenerator.qbroad, qbroad)
recipe.add_constraint(pdfgenerator.qdamp, qdamp)
recipe.add_constraint(pdfgenerator.qbroad, qbroad)
stru_parset = pdfgenerator.phase
spacegroupparams = constrainAsSpaceGroup(stru_parset, spacegroup)
for par in spacegroupparams.xyzpars:
recipe.addVar(
recipe.add_variable(
par, name=f"{pdfgenerator.name}_{par.name}", fixed=False
)
for par in spacegroupparams.latpars:
recipe.addVar(
recipe.add_variable(
par, name=f"{pdfgenerator.name}_{par.name}", fixed=False
)
for par in spacegroupparams.adppars:
recipe.addVar(
recipe.add_variable(
par, name=f"{pdfgenerator.name}_{par.name}", fixed=False
)
recipe.fithooks[0].verbose = 0
Expand All @@ -247,7 +247,7 @@ def add_contribution_variables(self, variable_names):
e.g. 's0' for scale factor.
"""
for var_name in variable_names:
self.recipe.addVar(
self.recipe.add_variable(
getattr(self.contribution, var_name),
name=var_name,
fixed=False,
Expand Down Expand Up @@ -275,7 +275,7 @@ def set_initial_variable_values(self, variable_name_to_value: dict):
Mapping from recipe variable names to new values.
"""
for vname, vvalue in variable_name_to_value.items():
self.recipe._parameters[vname].setValue(vvalue)
self.recipe._parameters[vname].set_value(vvalue)

def get_results(self):
"""Return the current fit results as a JSON-compatible
Expand Down
Empty file.
229 changes: 229 additions & 0 deletions src/diffpy/apps/refinebase/parametric_model.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
import re

import networkx as nx

from diffpy.srfit.fitbase import FitContribution
from diffpy.srfit.fitbase.parameter import Parameter, ParameterProxy
from diffpy.srfit.pdf.pdfgenerator import PDFGenerator
from diffpy.srfit.structure import constrain_as_space_group
from diffpy.structure import Structure


class ParametricModel:
def __init__(self, name):
self.name = name
self.calc_obj = FitContribution(name)
self._graph = nx.DiGraph()
# all submodels will share the same profile
self._submodels = []

def _construct_parameter_graph(
self, parameterset, prefix="", old_graph=None
):
parent_name = f"{prefix}{parameterset.name}"
self._graph.add_node(parent_name, parameter=parameterset)
for par in parameterset._iter_local_parameters(regexp=re.compile("")):
par_node_id = f"{parent_name}.{par.name}"
if not old_graph or par_node_id not in old_graph.nodes:
self._graph.add_node(
par_node_id,
parameter=par,
constrained_or_constant=False,
)
else:
self._graph.add_node(
par_node_id,
parameter=par,
constrained_or_constant=old_graph.nodes[par_node_id][
"constrained_or_constant"
],
)
self._graph.add_edge(parent_name, par_node_id)
for obj in parameterset._iter_managed_parameter_containers():
if hasattr(obj, "_iter_managed_parameter_containers"):
child_name = f"{parent_name}.{obj.name}"
# obj is handled as unconstrained by default
self._graph.add_node(
child_name,
parameter=None,
constrained_or_constant=False,
)
self._graph.add_edge(parent_name, child_name)
self._construct_parameter_graph(
obj,
prefix=f"{parent_name}.",
)

def register_submodel(self, submodel, symbol=None):
if not isinstance(self, ParametricModelEquation):
raise ValueError(
"Submodels can only be registered to "
"ParametricModelEquation instance."
)
if symbol is None:
symbol = submodel.name
if symbol in self.calc_obj._parameters:
self.calc_obj._remove_parameter(self.calc_obj._parameters[symbol])
if isinstance(submodel, ParametricModelPDF):
self.calc_obj.add_profile_generator(submodel.calc_obj)
elif isinstance(submodel, ParametricModelEquation):
self.calc_obj._eqfactory.registerOperator(
symbol, submodel.calc_obj._eq
)
self.calc_obj.add_parameter_set(submodel.calc_obj)
else:
raise NotImplementedError(
"Only ParametricModelPDF and ParametricModelEquation "
"instances are supported to be registered as submodels."
)
if self.equation_str is not None:
self.calc_obj.set_equation(self.equation_str)
self._submodels.append(submodel)
self._rebuild_graph()

def process_meta_data(self, meta):
if hasattr(self.calc_obj, "process_meta_data"):
self.calc_obj.process_meta_data(meta)

@property
def parameters(self):
return {
par_node_id: self._graph.nodes[par_node_id]["parameter"]
for par_node_id in self._graph.nodes
if isinstance(
self._graph.nodes[par_node_id]["parameter"], Parameter
)
}

@property
def independent_parameters(self):
return {
par_node_id: self._graph.nodes[par_node_id]["parameter"]
for par_node_id in self._graph.nodes
if isinstance(
self._graph.nodes[par_node_id]["parameter"], Parameter
)
and not (
(
hasattr(
self._graph.nodes[par_node_id]["parameter"], "const"
)
and self._graph.nodes[par_node_id]["parameter"].const
)
or self._graph.nodes[par_node_id]["parameter"].constrained
# NOTE: this is a workaround for the constraints not reflected
# in par.constrained
or self._graph.nodes[par_node_id]["constrained_or_constant"]
)
}

def set_profile(self, profile):
self.calc_obj.set_profile(profile)
for submodel in self._submodels:
if hasattr(submodel, "set_profile"):
submodel.set_profile(profile)
self._rebuild_graph()

def _rebuild_graph(self):
old_graph = self._graph
self._graph.clear()
self._construct_parameter_graph(
self.calc_obj, prefix="", old_graph=old_graph
)

def evaluate(self):
return self.calc_obj._eq()


class ParametricModelEquation(ParametricModel):
def __init__(self, name, equation_str=None):
super().__init__(name=name)
self.equation_str = None
if equation_str:
self.equation_str = equation_str
self.calc_obj.set_equation(equation_str)
self._rebuild_graph()

@property
def _contribution(self):
return self.calc_obj

def set_equation(self, equation_str):
self.equation_str = equation_str
self.calc_obj.set_equation(equation_str)
self._rebuild_graph()


class ParametricModelPDF(ParametricModel):
def __init__(self, name, structure: Structure):
super().__init__(name=name)
self.calc_obj = PDFGenerator(name)
self.calc_obj.setStructure(structure)
self._rebuild_graph()
self._hide_dependent_parameters()

def _hide_dependent_parameters(self):
dependent_par_names = [
r"\.U21$",
r"\.U31$",
r"\.U32$", # U21=U12, U31=U13, U32=U23
r"\.Biso",
r"\.B\d{2}", # Bij = Uij * 8 * pi^2
r"\.occupancy$", # occupancy=oc
]
regex = re.compile("|".join(dependent_par_names))
for par_name in self.parameters.keys():
if regex.search(par_name):
self._graph.nodes[par_name]["constrained_or_constant"] = True

def constrain_symmetry(self, spacegroup_symbol):
space_group_parset = constrain_as_space_group(
self.calc_obj.phase, spacegroup_symbol
)
# hide constrained parameters in the graph
symmetry_par_names = [
r"\.a$",
r"\.b$",
r"\.c$",
r"\.alpha$",
r"\.beta$",
r"\.gamma$",
r"\.x$",
r"\.y$",
r"\.z$",
r"\.Uiso$",
r"\.U11$",
r"\.U22$",
r"\.U33$",
r"\.U12$",
r"\.U13$",
r"\.U23$",
]
free_variables = []
for latpar in space_group_parset.latpars:
free_variables.append(latpar)
for adpar in space_group_parset.adppars:
free_variables.append(adpar)
for xyzpar in space_group_parset.xyzpars:
free_variables.append(xyzpar)
for i in range(len(free_variables)):
while isinstance(free_variables[i], ParameterProxy):
free_variables[i] = free_variables[i].par
symmetry_par_regex = re.compile("|".join(symmetry_par_names))
for par_name, par in self.parameters.items():
if symmetry_par_regex.search(par_name):
while isinstance(par, ParameterProxy):
par = par.par
if par not in free_variables:
self._graph.nodes[par_name][
"constrained_or_constant"
] = True

def set_qmin(self, qmin: float):
self.calc_obj.setQmin(qmin)

def set_qmax(self, qmax: float):
self.calc_obj.setQmax(qmax)

def set_scattering_type(self, scattering_type: str):
self.calc_obj.setScatteringType(scattering_type)
Loading
Loading