diff --git a/.codespell/ignore_words.txt b/.codespell/ignore_words.txt index 04b4fcf..70174b7 100644 --- a/.codespell/ignore_words.txt +++ b/.codespell/ignore_words.txt @@ -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 diff --git a/news/refinement.rst b/news/refinement.rst new file mode 100644 index 0000000..052eb7f --- /dev/null +++ b/news/refinement.rst @@ -0,0 +1,23 @@ +**Added:** + +* Add more flexible interface to utilize ``diffpy.srfit``. + +**Changed:** + +* + +**Deprecated:** + +* + +**Removed:** + +* + +**Fixed:** + +* + +**Security:** + +* diff --git a/requirements/conda.txt b/requirements/conda.txt index 61d392d..38287db 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -6,3 +6,5 @@ pyyaml diffpy.srfit diffpy.srreal diffpy.structure +networkx +mcp[cli] diff --git a/src/diffpy/__init__.py b/src/diffpy/__init__.py index 2d7de20..e69de29 100644 --- a/src/diffpy/__init__.py +++ b/src/diffpy/__init__.py @@ -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. -# -############################################################################## diff --git a/src/diffpy/apps/pdfadapter.py b/src/diffpy/apps/pdfadapter.py index 5ea27ac..2c734b5 100644 --- a/src/diffpy/apps/pdfadapter.py +++ b/src/diffpy/apps/pdfadapter.py @@ -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] @@ -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( @@ -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 @@ -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) ): @@ -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 @@ -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, @@ -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 diff --git a/src/diffpy/apps/refinebase/__init__.py b/src/diffpy/apps/refinebase/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/diffpy/apps/refinebase/parametric_model.py b/src/diffpy/apps/refinebase/parametric_model.py new file mode 100644 index 0000000..07d9c24 --- /dev/null +++ b/src/diffpy/apps/refinebase/parametric_model.py @@ -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) diff --git a/src/diffpy/apps/refinebase/refinement_server.py b/src/diffpy/apps/refinebase/refinement_server.py new file mode 100644 index 0000000..0a9779c --- /dev/null +++ b/src/diffpy/apps/refinebase/refinement_server.py @@ -0,0 +1,254 @@ +import uuid +from typing import Annotated + +from mcp.server import MCPServer + +from diffpy.apps.refinebase.refinement_session import RefinementSession + +session = RefinementSession() +mcp = MCPServer("diffpy.apps") + + +@mcp.tool() +async def add_pdf_profile( + profile_path: Annotated[str, "Path to the PDF profile file"], + profile_name: Annotated[str, "Unique name for the profile"] = None, +) -> str: + if profile_name is None: + profile_name = str(uuid.uuid4()) + """Add a pdf profile to the refinement session.""" + from diffpy.apps.refinebase.util import ( + get_pdf_profile, + ) + + profile = get_pdf_profile(profile_path) + session.add_profile(profile, profile_name=profile_name) + return f"Profile {profile_name} added successfully." + + +@mcp.tool() +async def add_dat_profile( + profile_path: Annotated[str, "Path to the .dat profile file"], + profile_name: Annotated[str, "Unique name for the profile"], +) -> str: + """Add a profile to the refinement session. + + The profile should satisfy that + xarray, yarray = np.loadtxt(profile_path)""" + from diffpy.apps.refinebase.util import ( + get_dat_profile, + ) + + if profile_name is None: + profile_name = str(uuid.uuid4()) + + profile = get_dat_profile(profile_path) + session.add_profile(profile, profile_name=profile_name) + return f"Profile {profile_name} added successfully." + + +@mcp.tool() +def add_text_profile( + xarray: Annotated[list, "X-values of the profile"], + yarray: Annotated[list, "Y-values of the profile"], + dx: Annotated[list, "Uncertainties in the x-values"] = None, + dy: Annotated[list, "Uncertainties in the y-values"] = None, + profile_name: Annotated[str, "Unique name for the profile"] = None, +) -> str: + """Add a profile to the refinement session. + + The profile is constructed from the provided xarray and yarray data.""" + from diffpy.apps.refinebase.util import ( + get_text_profile, + ) + + if profile_name is None: + profile_name = str(uuid.uuid4()) + + profile = get_text_profile(xarray, yarray, dx=dx, dy=dy) + session.add_profile(profile, profile_name=profile_name) + return f"Profile {profile_name} added successfully." + + +@mcp.tool() +async def remove_profile( + profile_name: Annotated[str, "Name of the profile to remove"], +) -> str: + """Remove a profile from the refinement session.""" + session.remove_profile(profile_name) + return f"Profile {profile_name} removed successfully." + + +@mcp.tool() +async def remove_model( + model_name: Annotated[str, "Name of the model to remove"], +) -> str: + """Remove a model from the refinement session.""" + session.remove_model(model_name) + return f"Model {model_name} removed successfully." + + +@mcp.tool() +async def add_pdf_model( + structure_path: Annotated[str, "Path to the structure file"], + model_name: Annotated[str, "Name of the parametric model"] = uuid.uuid4(), +) -> str: + """Add a PDF parametric model to the refinement session.""" + from diffpy.apps.refinebase.util import get_pdf_model + + pdf_model = get_pdf_model(structure_path=structure_path, name=model_name) + session.add_model(pdf_model) + return f"Model {pdf_model.name} added successfully." + + +@mcp.tool() +async def add_equation_model( + equation: Annotated[str, "Equation for the parametric model"], + model_name: Annotated[str, "Name of the parametric model"] = uuid.uuid4(), +) -> str: + """Add an equation-based parametric model to the refinement session.""" + from diffpy.apps.refinebase.parametric_model import ParametricModelEquation + + equation_model = ParametricModelEquation( + name=model_name, equation_str=equation + ) + session.add_model(equation_model) + return f"Model {equation_model.name} added successfully." + + +@mcp.tool() +async def list_profiles() -> list[str]: + """List all profiles in the refinement session.""" + return [str(profile_id) for profile_id in session.profiles_dict.keys()] + + +@mcp.tool() +async def list_models() -> list[str]: + """List all models in the refinement session.""" + return [str(model_id) for model_id in session.models_dict.keys()] + + +@mcp.tool() +async def combine_models( + parent_model_name: Annotated[str, "Name of the parent parametric model"], + child_model_name: Annotated[str, "Name of the child parametric model"], + symbol: Annotated[ + str, "Symbol to use for child model in the parent model's equation" + ], +) -> str: + """ + Combine two parametric models by registering the child to the parent model. + """ + if parent_model_name not in session.models_dict: + raise ValueError( + f"Parent model with ID {parent_model_name} does not exist." + ) + + if child_model_name not in session.models_dict: + raise ValueError( + f"Child model with ID {child_model_name} does not exist." + ) + + parent_model = session.models_dict[parent_model_name] + child_model = session.models_dict[child_model_name] + + parent_model.register_submodel(symbol, child_model) + + return ( + f"Models {parent_model_name} and " + f"{child_model_name} combined successfully." + ) + + +@mcp.tool() +async def set_model_param_value( + param_name: Annotated[str, "Name of the parameter to set"], + value: Annotated[float, "Value to set for the parameter"], +) -> str: + """ + Set the value of a specific parameter in a parametric model. + """ + + variable = session.get_variable(param_name) + variable.set_value(value) + + return f"Parameter '{param_name}' is set to {value}." + + +@mcp.tool() +async def list_model_parameters( + model_name: Annotated[str, "Name of the parametric model"], +) -> str: + """ + List all parameters of a specific parametric model. + """ + if model_name not in session.models_dict: + raise ValueError(f"Model with ID {model_name} does not exist.") + + model = session.models_dict[model_name] + parameters = { + node_id: par.value for node_id, par in model.parameters.items() + } + return f"Parameters for model '{model_name}': {parameters}" + + +@mcp.tool() +async def get_model_value( + model_name: Annotated[str, "Name of the parametric model"], +) -> str: + """ + Get the value of a specific parametric model. + """ + if model_name not in session.models_dict: + raise ValueError(f"Model with ID {model_name} does not exist.") + + model = session.models_dict[model_name] + value = model.evaluate() + + return f"Value for model '{model_name}': {value}" + + +@mcp.tool() +async def refine( + profile_names: Annotated[ + list[str], "List of profile IDs to use in the refinement" + ], + model_names: Annotated[ + list[str], "List of model IDs to use in the refinement" + ], + variable_names: Annotated[list[str], "List of variable names to refine"], + weights: Annotated[list[float], "List of weights for each profile"] = None, + initial_values: Annotated[ + list[float], "List of initial values for each variable" + ] = None, +) -> str: + """ + Perform a refinement using the specified profiles, models, and variables. + """ + # Retrieve profiles and models from session using provided IDs + + profile_objs = [session.profiles_dict[pid] for pid in profile_names] + model_objs = [session.models_dict[mid] for mid in model_names] + variable_objs = [ + session.get_variable(var_name) for var_name in variable_names + ] + + # Perform refinement + session._solve( + profile_objs, + model_objs, + variable_objs, + weights=weights, + initial_values=initial_values, + ) + + # Collect refined variable values + refined_values = {var.name: var.value for var in variable_objs} + + return ( + f"Refinement completed successfully. Refined values: {refined_values}" + ) + + +if __name__ == "__main__": + mcp.run() diff --git a/src/diffpy/apps/refinebase/refinement_session.py b/src/diffpy/apps/refinebase/refinement_session.py new file mode 100644 index 0000000..117f025 --- /dev/null +++ b/src/diffpy/apps/refinebase/refinement_session.py @@ -0,0 +1,85 @@ +import uuid +from collections import OrderedDict + +import numpy +from scipy.optimize import least_squares + +from diffpy.apps.refinebase.parametric_model import ParametricModel +from diffpy.srfit.fitbase import ( + FitRecipe, + Profile, +) + + +class RefinementSession: + def __init__(self): + self.recipes_dict = OrderedDict() + self.profiles_dict = OrderedDict() + self.models_dict = OrderedDict() + + def add_profile(self, profile: Profile, profile_name: str = None): + if profile in self.profiles_dict.values(): + raise ValueError("Profile already exists in the session.") + if profile_name is None: + profile_name = str(uuid.uuid4()) + self.profiles_dict[profile_name] = profile + + def add_model(self, model: ParametricModel): + if model in self.models_dict.values(): + raise ValueError("Model already exists in the session.") + self.models_dict[model.name] = model + + def remove_profile(self, profile_name: str): + if profile_name not in self.profiles_dict: + raise ValueError(f"Profile with ID {profile_name} does not exist.") + del self.profiles_dict[profile_name] + + def remove_model(self, model_name: str): + if model_name not in self.models_dict: + raise ValueError(f"Model with ID {model_name} does not exist.") + del self.models_dict[model_name] + + def get_variable(self, variable_name): + objs = variable_name.split(".") + if objs[0] not in self.models_dict: + raise ValueError(f"Model '{objs[0]}' not found in the session.") + if variable_name not in self.models_dict[objs[0]].parameters: + raise ValueError( + f"Variable '{variable_name}' not found in " + f"the model '{objs[0]}'." + ) + return self.models_dict[objs[0]].parameters[variable_name] + + def _solve( + self, + profiles, + models, + variables, + id=uuid.uuid4(), + weights=None, + initial_values=None, + ): + recipe = FitRecipe() + self.recipes_dict[id] = recipe + if weights is None: + weights = numpy.ones(len(profiles)) / len(profiles) + for i in range(len(models)): + models[i].set_profile(profiles[i]) + recipe.add_contribution(models[i]._contribution, weight=weights[i]) + + # Add variables + if initial_values is not None: + for var, val in zip(variables, initial_values): + var.value = val + if len(set([var.name for var in variables])) != len(variables): + raise ValueError( + "Duplicate variable names found. Please ensure that " + "each variable to be refined has a unique name." + ) + for var in variables: + recipe.add_variable(var) + # Refine the recipe + recipe.fix("all") + for i in range(len(variables)): + recipe.free(variables[i].name) + least_squares(recipe.residual, recipe.getValues(), x_scale="jac") diff --git a/src/diffpy/apps/refinebase/util.py b/src/diffpy/apps/refinebase/util.py new file mode 100644 index 0000000..3a9bd9b --- /dev/null +++ b/src/diffpy/apps/refinebase/util.py @@ -0,0 +1,37 @@ +def get_pdf_profile(profile_path: str): + from diffpy.srfit.fitbase import Profile + from diffpy.srfit.pdf import PDFParser + + profile = Profile() + parser = PDFParser() + parser.parse_file(profile_path) + profile.load_parsed_data(parser) + return profile + + +def get_dat_profile(profile_path: str): + from diffpy.srfit.fitbase import Profile + + profile = Profile() + profile.loadtxt(profile_path) + + return profile + + +def get_text_profile(xarray, yarray, dx=None, dy=None): + from diffpy.srfit.fitbase import Profile + + profile = Profile() + profile.setObservedProfile(xarray, yarray, dx=dx, dy=dy) + + return profile + + +def get_pdf_model(structure_path: str, name="pdf"): + from diffpy.apps.refinebase.parametric_model import ParametricModelPDF + from diffpy.structure import Structure + + stru = Structure() + stru.read(structure_path) + pdf_model = ParametricModelPDF(name, structure=stru) + return pdf_model diff --git a/tests/conftest.py b/tests/conftest.py index e3b6313..79aebf1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,7 +1,19 @@ import json from pathlib import Path +import numpy import pytest +from helper import make_cmi_recipe +from scipy.optimize import least_squares + +from diffpy.apps.refinebase.parametric_model import ( + ParametricModelPDF, +) +from diffpy.srfit.fitbase import ( + Profile, +) +from diffpy.srfit.pdf import PDFParser +from diffpy.structure import Structure @pytest.fixture @@ -17,3 +29,82 @@ def user_filesystem(tmp_path): json.dump(home_config_data, f) yield tmp_path + + +@pytest.fixture +def nested_sine_model(): + from diffpy.apps.refinebase.parametric_model import ParametricModelEquation + + submodel = ParametricModelEquation("sub", "a*x") + model = ParametricModelEquation("main", "A*sin(u)") + model.register_submodel(symbol="u", submodel=submodel) + return model, submodel + + +@pytest.fixture +def sine_profile(): + xobs = numpy.linspace(-numpy.pi, numpy.pi, 100) + yobs = numpy.sin(xobs) + 1e-3 * numpy.random.normal(size=xobs.shape) + sine_profile = Profile() + sine_profile.setObservedProfile(xobs, yobs) + return sine_profile + + +@pytest.fixture +def ni_pdf_model(): + stru = Structure() + structure_path = "tests/data/Ni.cif" + stru.read(structure_path) + pdf_model = ParametricModelPDF("pdf", structure=stru) + return pdf_model + + +@pytest.fixture +def ni_pdf_profile(): + profile_path = "tests/data/Ni.gr" + profile = Profile() + parser = PDFParser() + parser.parse_file(profile_path) + profile.load_parsed_data(parser) + profile.set_calculation_range(xmin=1.5, xmax=50, dx=0.01) + profile.meta["qmin"] = 0.1 + return profile + + +@pytest.fixture +def ni_refined_parameters(): + structure_path = Path(__file__).parent / "data" / "Ni.cif" + profile_path = Path(__file__).parent / "data" / "Ni.gr" + initial_pv_dict = { + "s0": 0.4, + "qdamp": 0.04, + "qbroad": 0.02, + "G1_a": 3.52, + "G1_delta2": 2, + "G1_Uiso_0": 0.005, + } + variables_to_refine = [ + "G1_a", + "s0", + "G1_Uiso_0", + "G1_delta2", + "qdamp", + "qbroad", + ] + diffpycmi_recipe = make_cmi_recipe( + str(structure_path), str(profile_path), initial_pv_dict + ) + diffpycmi_recipe.fithooks[0].verbose = 0 + diffpycmi_recipe.fix("all") + + for var_name in variables_to_refine: + diffpycmi_recipe.free(var_name) + least_squares( + diffpycmi_recipe.residual, + diffpycmi_recipe.values, + x_scale="jac", + ) + diffpy_pv_dict = {} + for pname, parameter in diffpycmi_recipe._parameters.items(): + diffpy_pv_dict[pname] = parameter.value + return diffpy_pv_dict diff --git a/tests/data/sine.dat b/tests/data/sine.dat new file mode 100644 index 0000000..5c4e92c --- /dev/null +++ b/tests/data/sine.dat @@ -0,0 +1,400 @@ +-6.283185307179586232e+00 5.333472993497439391e-02 +-6.251690643985703844e+00 4.730056680876025543e-02 +-6.220195980791820567e+00 6.674570663149917427e-02 +-6.188701317597938178e+00 8.163496149342541752e-02 +-6.157206654404055790e+00 5.765535646179940910e-02 +-6.125711991210173402e+00 1.362339072376442528e-01 +-6.094217328016290125e+00 1.220400021248826683e-01 +-6.062722664822407737e+00 2.071501486063853370e-01 +-6.031228001628525348e+00 2.171193621378014194e-01 +-5.999733338434642960e+00 2.591809174618869283e-01 +-5.968238675240759683e+00 2.440214685406498218e-01 +-5.936744012046877295e+00 3.844927167452896200e-01 +-5.905249348852994906e+00 3.332980129599112384e-01 +-5.873754685659111630e+00 3.730055499203158154e-01 +-5.842260022465229241e+00 4.607970012517633718e-01 +-5.810765359271346853e+00 4.532285005410850998e-01 +-5.779270696077464464e+00 5.173537937900670736e-01 +-5.747776032883581188e+00 5.883332618147742998e-01 +-5.716281369689698799e+00 5.913440547536299441e-01 +-5.684786706495816411e+00 4.963786760438377854e-01 +-5.653292043301933134e+00 6.396698668434632751e-01 +-5.621797380108050746e+00 5.808737866238390390e-01 +-5.590302716914168357e+00 6.266708299351340106e-01 +-5.558808053720285969e+00 6.227004467993523251e-01 +-5.527313390526402692e+00 6.823731484804475000e-01 +-5.495818727332520304e+00 7.340925226123561353e-01 +-5.464324064138637915e+00 7.302709722170439566e-01 +-5.432829400944754639e+00 8.683837400411715812e-01 +-5.401334737750872250e+00 6.895832548205809154e-01 +-5.369840074556989862e+00 8.215986611475818435e-01 +-5.338345411363107473e+00 7.879480256710136299e-01 +-5.306850748169224197e+00 9.595110296491765611e-01 +-5.275356084975341808e+00 7.299891274658213414e-01 +-5.243861421781459420e+00 9.856307651214625265e-01 +-5.212366758587576143e+00 8.012760024963282435e-01 +-5.180872095393693755e+00 9.416275734260856067e-01 +-5.149377432199811366e+00 8.884518200864035542e-01 +-5.117882769005928978e+00 9.531796693250517150e-01 +-5.086388105812045701e+00 9.782957169622655824e-01 +-5.054893442618163313e+00 9.455210356411575257e-01 +-5.023398779424280924e+00 8.706119647183223620e-01 +-4.991904116230397648e+00 9.780865472617736867e-01 +-4.960409453036515259e+00 9.962353893266722160e-01 +-4.928914789842632871e+00 9.637111112136874658e-01 +-4.897420126648750482e+00 9.982173974726372689e-01 +-4.865925463454867206e+00 1.003147631994909750e+00 +-4.834430800260984817e+00 9.512796572746163859e-01 +-4.802936137067102429e+00 1.082386181777095668e+00 +-4.771441473873219152e+00 1.070112366754480071e+00 +-4.739946810679336764e+00 1.041925366211090109e+00 +-4.708452147485454375e+00 1.087654449509526611e+00 +-4.676957484291571987e+00 1.066061239773753666e+00 +-4.645462821097689599e+00 1.019283108439490437e+00 +-4.613968157903806322e+00 1.021109744295398025e+00 +-4.582473494709923933e+00 1.019643704242726878e+00 +-4.550978831516040657e+00 9.851661156523525698e-01 +-4.519484168322158268e+00 1.033390072637789370e+00 +-4.487989505128275880e+00 1.051638981953632701e+00 +-4.456494841934393492e+00 9.517347418449753071e-01 +-4.425000178740511103e+00 9.599397437158793123e-01 +-4.393505515546627826e+00 9.259938294232830103e-01 +-4.362010852352745438e+00 9.456359598737877903e-01 +-4.330516189158862161e+00 8.943839615779231034e-01 +-4.299021525964979773e+00 8.842597584163086610e-01 +-4.267526862771097385e+00 9.310524867043514785e-01 +-4.236032199577214996e+00 8.431128127481050116e-01 +-4.204537536383332608e+00 8.497795747620827678e-01 +-4.173042873189449331e+00 8.772585612073843242e-01 +-4.141548209995566943e+00 7.530639725678698326e-01 +-4.110053546801683666e+00 8.937803155702943592e-01 +-4.078558883607801278e+00 8.264313830386144089e-01 +-4.047064220413918889e+00 8.024375507067316171e-01 +-4.015569557220036501e+00 8.214380978198694283e-01 +-3.984074894026153668e+00 6.904198695129163399e-01 +-3.952580230832270836e+00 7.439012338732706731e-01 +-3.921085567638388447e+00 7.730044315034473312e-01 +-3.889590904444505615e+00 7.712891941726492950e-01 +-3.858096241250623226e+00 6.620484177142149651e-01 +-3.826601578056740394e+00 6.847804714873817478e-01 +-3.795106914862858005e+00 5.852841570004606586e-01 +-3.763612251668975173e+00 5.715312031949316696e-01 +-3.732117588475092340e+00 5.307797104173763714e-01 +-3.700622925281209952e+00 5.846302213621512589e-01 +-3.669128262087327119e+00 5.156872898311184450e-01 +-3.637633598893444731e+00 3.974016196517171640e-01 +-3.606138935699561898e+00 4.805438480014790903e-01 +-3.574644272505679510e+00 3.968475780637474304e-01 +-3.543149609311796677e+00 4.362874534365135948e-01 +-3.511654946117913845e+00 4.343331697068009700e-01 +-3.480160282924031456e+00 3.253504620557266525e-01 +-3.448665619730148624e+00 3.197758477717250747e-01 +-3.417170956536266235e+00 2.139248532312771067e-01 +-3.385676293342383403e+00 2.671251026291971975e-01 +-3.354181630148501014e+00 1.407863298190564216e-01 +-3.322686966954618182e+00 1.498456323271817325e-01 +-3.291192303760735793e+00 1.011040675029006297e-01 +-3.259697640566852961e+00 1.749477783973979739e-01 +-3.228202977372970128e+00 1.902819554604764096e-01 +-3.196708314179087740e+00 9.513929312934064908e-03 +-3.165213650985204907e+00 7.285066003464360507e-04 +-3.133718987791322519e+00 -8.361559701509049813e-02 +-3.102224324597439686e+00 5.297876588104493550e-02 +-3.070729661403557298e+00 -3.124925641987919694e-02 +-3.039234998209674465e+00 -6.705718346773389960e-02 +-3.007740335015792077e+00 -2.240713315568368391e-01 +-2.976245671821909244e+00 -1.727570161241142321e-01 +-2.944751008628026412e+00 -9.408741018694728730e-02 +-2.913256345434144023e+00 -2.424823907448965254e-01 +-2.881761682240261191e+00 -2.062160158105544316e-01 +-2.850267019046378802e+00 -2.837988747138451839e-01 +-2.818772355852495970e+00 -2.992285332985984980e-01 +-2.787277692658613582e+00 -2.882399490139626597e-01 +-2.755783029464730749e+00 -3.747294518534884378e-01 +-2.724288366270847916e+00 -4.074455775937312896e-01 +-2.692793703076965528e+00 -5.176146618181154402e-01 +-2.661299039883082695e+00 -4.701573418210351907e-01 +-2.629804376689200307e+00 -5.386867638254433377e-01 +-2.598309713495317474e+00 -4.994965794035008955e-01 +-2.566815050301435086e+00 -5.893586583325615136e-01 +-2.535320387107552254e+00 -5.402587202801102384e-01 +-2.503825723913669421e+00 -6.001532378759411035e-01 +-2.472331060719787033e+00 -6.524688559125335630e-01 +-2.440836397525904200e+00 -6.848915691610645284e-01 +-2.409341734332021812e+00 -6.405415348312248902e-01 +-2.377847071138138979e+00 -6.203595609073114803e-01 +-2.346352407944256591e+00 -7.554890400576492748e-01 +-2.314857744750373758e+00 -7.393459699815611019e-01 +-2.283363081556491370e+00 -7.431365063588601938e-01 +-2.251868418362608537e+00 -7.501553583981681594e-01 +-2.220373755168726149e+00 -6.756894603145571221e-01 +-2.188879091974842872e+00 -8.295146705420044508e-01 +-2.157384428780960484e+00 -7.560202667948484612e-01 +-2.125889765587078095e+00 -8.545510958578339533e-01 +-2.094395102393195707e+00 -8.535476106542244645e-01 +-2.062900439199312430e+00 -8.930160561088548166e-01 +-2.031405776005430042e+00 -9.095864437610129416e-01 +-1.999911112811547653e+00 -8.170980662101963166e-01 +-1.968416449617664377e+00 -9.259730579734212208e-01 +-1.936921786423781988e+00 -8.806209237765607600e-01 +-1.905427123229899600e+00 -9.087432800260908383e-01 +-1.873932460036017211e+00 -8.762467885556992631e-01 +-1.842437796842133935e+00 -9.176280691792784161e-01 +-1.810943133648251546e+00 -9.552143480940763487e-01 +-1.779448470454369158e+00 -1.005882970938458243e+00 +-1.747953807260486769e+00 -1.030090866293738250e+00 +-1.716459144066603493e+00 -1.035693889029885462e+00 +-1.684964480872721104e+00 -9.796592853136956158e-01 +-1.653469817678838716e+00 -1.037862979948626752e+00 +-1.621975154484955439e+00 -9.481858771511948447e-01 +-1.590480491291073051e+00 -9.636366654935387688e-01 +-1.558985828097190662e+00 -1.011184886111144543e+00 +-1.527491164903308274e+00 -1.078614094350218711e+00 +-1.495996501709424997e+00 -9.060519205623029926e-01 +-1.464501838515542609e+00 -1.002655387054679492e+00 +-1.433007175321660220e+00 -9.568249554002589141e-01 +-1.401512512127776944e+00 -9.284896397939189638e-01 +-1.370017848933894555e+00 -1.068426804300812050e+00 +-1.338523185740012167e+00 -9.814679801764907285e-01 +-1.307028522546129778e+00 -9.707156705056556589e-01 +-1.275533859352246502e+00 -9.567295825744793758e-01 +-1.244039196158364113e+00 -9.545800343040824476e-01 +-1.212544532964481725e+00 -9.689973339127266883e-01 +-1.181049869770598448e+00 -9.171643615137812233e-01 +-1.149555206576716060e+00 -7.945647407509961457e-01 +-1.118060543382833671e+00 -9.312590718056790173e-01 +-1.086565880188951283e+00 -9.451529682252668429e-01 +-1.055071216995068006e+00 -8.619259436726246504e-01 +-1.023576553801185618e+00 -9.134326796273872784e-01 +-9.920818906073032295e-01 -9.041659185529236087e-01 +-9.605872274134199529e-01 -8.474550531269914311e-01 +-9.290925642195375644e-01 -7.846865575776393875e-01 +-8.975979010256551760e-01 -8.265441754958452458e-01 +-8.661032378317727876e-01 -7.503379633083022693e-01 +-8.346085746378895109e-01 -7.946294753173039904e-01 +-8.031139114440071225e-01 -7.580543671107451420e-01 +-7.716192482501247341e-01 -7.528739666945439835e-01 +-7.401245850562414574e-01 -6.840920389074485186e-01 +-7.086299218623590690e-01 -5.898370127083051306e-01 +-6.771352586684766806e-01 -6.130114298254869531e-01 +-6.456405954745942921e-01 -5.421582738519427069e-01 +-6.141459322807110155e-01 -6.175603883643896452e-01 +-5.826512690868286271e-01 -5.178780251342394170e-01 +-5.511566058929462386e-01 -5.095682253637823234e-01 +-5.196619426990638502e-01 -5.087502303738271614e-01 +-4.881672795051805736e-01 -5.612261688386659397e-01 +-4.566726163112981851e-01 -3.909741827782163281e-01 +-4.251779531174157967e-01 -4.357892929826272521e-01 +-3.936832899235325200e-01 -4.409168765862667150e-01 +-3.621886267296501316e-01 -3.518254685009435390e-01 +-3.306939635357677432e-01 -3.436355721076164715e-01 +-2.991993003418853547e-01 -3.178658071503189797e-01 +-2.677046371480020781e-01 -2.480188690981060817e-01 +-2.362099739541196897e-01 -2.132367809444414286e-01 +-2.047153107602373012e-01 -1.805130274843314842e-01 +-1.732206475663540246e-01 -1.288680730985389544e-01 +-1.417259843724716362e-01 -9.058323720170644022e-02 +-1.102313211785892477e-01 -1.762372598909556776e-01 +-7.873665798470685928e-02 -1.031319045245991245e-01 +-4.724199479082358266e-02 -2.248723402791667075e-02 +-1.574733159694119422e-02 -1.019248259629703182e-02 +1.574733159694119422e-02 5.561015125613280596e-02 +4.724199479082447084e-02 2.850678047274404076e-02 +7.873665798470685928e-02 2.999745151604019111e-02 +1.102313211785892477e-01 1.561400734143876257e-01 +1.417259843724716362e-01 1.302267918949870373e-01 +1.732206475663549128e-01 6.785045455458592334e-02 +2.047153107602373012e-01 2.669477195929434443e-01 +2.362099739541196897e-01 1.842806727012889778e-01 +2.677046371480020781e-01 3.113115434177701202e-01 +2.991993003418853547e-01 2.822754874795110891e-01 +3.306939635357677432e-01 3.365286976034461697e-01 +3.621886267296501316e-01 4.297720577737871306e-01 +3.936832899235334082e-01 4.504576882792186154e-01 +4.251779531174157967e-01 4.257790570354693660e-01 +4.566726163112981851e-01 4.056662603318181293e-01 +4.881672795051805736e-01 5.192140315466554634e-01 +5.196619426990638502e-01 4.726952791730072434e-01 +5.511566058929462386e-01 4.956804196399551277e-01 +5.826512690868286271e-01 5.107578824303367071e-01 +6.141459322807119037e-01 5.482023251793258778e-01 +6.456405954745942921e-01 6.153580261081638136e-01 +6.771352586684766806e-01 5.543029255111912024e-01 +7.086299218623590690e-01 7.088344557009651492e-01 +7.401245850562423456e-01 6.356653778257231879e-01 +7.716192482501247341e-01 7.673458159133867973e-01 +8.031139114440071225e-01 7.066354583780416476e-01 +8.346085746378903991e-01 7.381698728238748064e-01 +8.661032378317727876e-01 7.498746647116933950e-01 +8.975979010256551760e-01 7.165366479557023771e-01 +9.290925642195375644e-01 7.382664416274424823e-01 +9.605872274134208411e-01 8.848588319225931809e-01 +9.920818906073032295e-01 7.955472227369922988e-01 +1.023576553801185618e+00 8.447340369606414212e-01 +1.055071216995068895e+00 8.889494243020082953e-01 +1.086565880188951283e+00 8.448711049043226007e-01 +1.118060543382833671e+00 8.939085720020327752e-01 +1.149555206576716060e+00 8.843083314823503294e-01 +1.181049869770599337e+00 9.515426278408282146e-01 +1.212544532964481725e+00 9.517631033849265343e-01 +1.244039196158364113e+00 8.445403868231657896e-01 +1.275533859352247390e+00 9.784882824996637973e-01 +1.307028522546129778e+00 9.357540430059432257e-01 +1.338523185740012167e+00 1.060796725445710909e+00 +1.370017848933894555e+00 9.246950959108836132e-01 +1.401512512127777832e+00 9.968238652066258032e-01 +1.433007175321660220e+00 9.735599894579468616e-01 +1.464501838515542609e+00 1.010035590115179627e+00 +1.495996501709424997e+00 1.009557336962188634e+00 +1.527491164903308274e+00 9.414509604964800271e-01 +1.558985828097190662e+00 9.706555159935927879e-01 +1.590480491291073051e+00 9.043408568724520258e-01 +1.621975154484956327e+00 1.038821399165152037e+00 +1.653469817678838716e+00 1.075344626837139828e+00 +1.684964480872721104e+00 1.013189559680832241e+00 +1.716459144066603493e+00 9.994075970669294984e-01 +1.747953807260486769e+00 8.798250886605673005e-01 +1.779448470454369158e+00 1.015192233030990154e+00 +1.810943133648251546e+00 9.631822954691046101e-01 +1.842437796842133935e+00 9.264165894676001933e-01 +1.873932460036016323e+00 9.483629352159634873e-01 +1.905427123229900488e+00 9.584441944685537784e-01 +1.936921786423782876e+00 8.110467073191865994e-01 +1.968416449617665265e+00 9.741318652872379413e-01 +1.999911112811547653e+00 8.513519706650380936e-01 +2.031405776005430042e+00 8.363049973814026261e-01 +2.062900439199312430e+00 8.550778795685389788e-01 +2.094395102393194819e+00 8.327541830147691115e-01 +2.125889765587078983e+00 9.226856006582023761e-01 +2.157384428780961372e+00 7.992997631081057985e-01 +2.188879091974843760e+00 8.480535044363670671e-01 +2.220373755168726149e+00 7.976456999502447021e-01 +2.251868418362608537e+00 7.975898775105769722e-01 +2.283363081556490926e+00 8.050987402228190204e-01 +2.314857744750373314e+00 7.520701257850432864e-01 +2.346352407944257479e+00 6.983811898701800525e-01 +2.377847071138139867e+00 7.970811344087292971e-01 +2.409341734332022256e+00 5.813226284302609459e-01 +2.440836397525904644e+00 6.134028296153083426e-01 +2.472331060719787033e+00 6.146775162628060896e-01 +2.503825723913669421e+00 6.166536127641077947e-01 +2.535320387107551809e+00 5.860653128988175808e-01 +2.566815050301435974e+00 6.099760012967703204e-01 +2.598309713495318363e+00 4.064563575340121027e-01 +2.629804376689200751e+00 5.044822483109848710e-01 +2.661299039883083140e+00 4.794187621766899388e-01 +2.692793703076965528e+00 3.892280066576866981e-01 +2.724288366270847916e+00 4.494834440666407582e-01 +2.755783029464730305e+00 3.692565771893440707e-01 +2.787277692658612693e+00 3.638689943321363196e-01 +2.818772355852496858e+00 3.090041453847930941e-01 +2.850267019046379247e+00 3.199056922043522766e-01 +2.881761682240261635e+00 1.420298953349984350e-01 +2.913256345434144023e+00 1.934074762371242318e-01 +2.944751008628026412e+00 1.896192717716490561e-01 +2.976245671821908800e+00 1.849620414727209949e-01 +3.007740335015791189e+00 1.473428426390076540e-01 +3.039234998209675354e+00 5.772470185919462149e-03 +3.070729661403557742e+00 8.670928917959888627e-02 +3.102224324597440130e+00 1.505944256909951817e-01 +3.133718987791322519e+00 1.057205355507669434e-02 +3.165213650985204907e+00 -1.961223285869023725e-02 +3.196708314179087296e+00 -1.074376846044653444e-01 +3.228202977372969684e+00 -4.469753991353946077e-02 +3.259697640566853849e+00 -8.557136958347436040e-02 +3.291192303760736237e+00 -2.092066282696974922e-01 +3.322686966954618626e+00 -2.627579973998567242e-01 +3.354181630148501014e+00 -2.149854955278473201e-01 +3.385676293342383403e+00 -1.911385859942967558e-01 +3.417170956536265791e+00 -2.972566413894532356e-01 +3.448665619730148180e+00 -1.993232814249967233e-01 +3.480160282924032344e+00 -2.616826036489552920e-01 +3.511654946117914733e+00 -2.960827278840391097e-01 +3.543149609311797121e+00 -4.117795647667891745e-01 +3.574644272505679510e+00 -4.162677964300542932e-01 +3.606138935699561898e+00 -3.927339896663378238e-01 +3.637633598893444287e+00 -5.022086973125871046e-01 +3.669128262087326675e+00 -4.883150076528400829e-01 +3.700622925281210840e+00 -5.875789176611229125e-01 +3.732117588475093228e+00 -5.412766498594321352e-01 +3.763612251668975617e+00 -5.386774388369347522e-01 +3.795106914862858005e+00 -6.668850275372711911e-01 +3.826601578056740394e+00 -5.917011555561028091e-01 +3.858096241250622782e+00 -5.871050830140525090e-01 +3.889590904444505171e+00 -7.434780946484742525e-01 +3.921085567638389335e+00 -6.896959418477949244e-01 +3.952580230832271724e+00 -7.820370931495217448e-01 +3.984074894026154112e+00 -7.244114971520748680e-01 +4.015569557220036501e+00 -7.586192837693910285e-01 +4.047064220413918889e+00 -8.665734694141173922e-01 +4.078558883607801278e+00 -7.588937316227160057e-01 +4.110053546801683666e+00 -8.668686906153582061e-01 +4.141548209995567831e+00 -9.034050818378567271e-01 +4.173042873189450219e+00 -8.549632716610970906e-01 +4.204537536383332608e+00 -8.099097720426193803e-01 +4.236032199577214996e+00 -8.371986062495411218e-01 +4.267526862771097385e+00 -9.298874922784227115e-01 +4.299021525964979773e+00 -8.950890249792161635e-01 +4.330516189158862161e+00 -9.811384722945486470e-01 +4.362010852352746326e+00 -9.711891618582448871e-01 +4.393505515546628715e+00 -9.383757903140135248e-01 +4.425000178740511103e+00 -1.000729863724425517e+00 +4.456494841934393492e+00 -1.001826600982660631e+00 +4.487989505128275880e+00 -9.817740824869456429e-01 +4.519484168322158268e+00 -9.146847790507851572e-01 +4.550978831516040657e+00 -1.126770422349620215e+00 +4.582473494709924822e+00 -9.607875710439345296e-01 +4.613968157903807210e+00 -9.696696335892329710e-01 +4.645462821097689599e+00 -1.006070120377763333e+00 +4.676957484291571987e+00 -9.521118417468951156e-01 +4.708452147485454375e+00 -1.039690433541499814e+00 +4.739946810679336764e+00 -9.850761455793340948e-01 +4.771441473873219152e+00 -1.003017940098269056e+00 +4.802936137067103317e+00 -9.756527171870794657e-01 +4.834430800260985706e+00 -1.032874492433806246e+00 +4.865925463454868094e+00 -1.016765811326304281e+00 +4.897420126648750482e+00 -1.036694520869491765e+00 +4.928914789842632871e+00 -9.725114451188217046e-01 +4.960409453036515259e+00 -9.588546622770659500e-01 +4.991904116230397648e+00 -1.052174923539183737e+00 +5.023398779424280036e+00 -9.797463478785696234e-01 +5.054893442618164201e+00 -9.009895922564135073e-01 +5.086388105812046589e+00 -9.258864012489034234e-01 +5.117882769005928978e+00 -9.720017644361897968e-01 +5.149377432199811366e+00 -8.964146762085127840e-01 +5.180872095393693755e+00 -8.733433671649337704e-01 +5.212366758587576143e+00 -8.394766771199186906e-01 +5.243861421781458532e+00 -8.536185191492570157e-01 +5.275356084975342696e+00 -7.684525468952877469e-01 +5.306850748169225085e+00 -7.915153807401620334e-01 +5.338345411363107473e+00 -8.037784602129141032e-01 +5.369840074556989862e+00 -8.074571249585872357e-01 +5.401334737750872250e+00 -7.935669005816983201e-01 +5.432829400944754639e+00 -7.185997545450047985e-01 +5.464324064138637027e+00 -6.733472820114593249e-01 +5.495818727332521192e+00 -6.944689019800011076e-01 +5.527313390526403580e+00 -7.180489238550267217e-01 +5.558808053720285969e+00 -6.509581779709366911e-01 +5.590302716914168357e+00 -5.932740892008087075e-01 +5.621797380108050746e+00 -6.228024299124416130e-01 +5.653292043301933134e+00 -6.411136926038654149e-01 +5.684786706495815523e+00 -6.083455050526439534e-01 +5.716281369689699687e+00 -5.972604330508973769e-01 +5.747776032883582076e+00 -4.425046155799902681e-01 +5.779270696077464464e+00 -6.078718480593998663e-01 +5.810765359271346853e+00 -3.767735728047209021e-01 +5.842260022465229241e+00 -4.979039355957451951e-01 +5.873754685659111630e+00 -3.677547380621239981e-01 +5.905249348852994018e+00 -2.818193350864402480e-01 +5.936744012046878183e+00 -3.446315350032238545e-01 +5.968238675240760571e+00 -2.703450873786260589e-01 +5.999733338434642960e+00 -3.136916165827080549e-01 +6.031228001628525348e+00 -3.296620922232299522e-01 +6.062722664822407737e+00 -2.042509801240406375e-01 +6.094217328016290125e+00 -1.754604529873591279e-01 +6.125711991210172513e+00 -1.776470618976437599e-01 +6.157206654404056678e+00 -9.610723412669471000e-02 +6.188701317597939067e+00 -1.138715211751845091e-01 +6.220195980791821455e+00 -9.664494503363602051e-02 +6.251690643985703844e+00 3.625998729126720527e-02 +6.283185307179586232e+00 6.640668213601869443e-03 diff --git a/tests/test_parametric_model.py b/tests/test_parametric_model.py new file mode 100644 index 0000000..a8ed931 --- /dev/null +++ b/tests/test_parametric_model.py @@ -0,0 +1,88 @@ +import numpy +import pytest + +from diffpy.apps.refinebase.parametric_model import ( + ParametricModelEquation, + ParametricModelPDF, +) +from diffpy.srfit.fitbase import ( + Profile, +) +from diffpy.srfit.pdf import PDFParser +from diffpy.structure import Structure + + +def test_parametric_model_graph(): + # C1: Create a nested parametric model + # Expect the graph to be constructed correctly + model = ParametricModelEquation("main", "A*sin(u)") + submodel = ParametricModelEquation("sub", "a*x") + model.register_submodel(symbol="u", submodel=submodel) + expected_parameters = ["main.A", "main.sub.a", "main.sub.x"] + actual_parameters = list(model.parameters.keys()) + assert set(expected_parameters) == set(actual_parameters) + expected_edges = [ + ("main", "main.A"), + ("main", "main.sub"), + ("main.sub", "main.sub.a"), + ("main.sub", "main.sub.x"), + ] + actual_edges = list(model._graph.edges) + assert set(expected_edges) == set(actual_edges) + + +def test_parametric_model_parameter_access(nested_sine_model): + # C1: Create a nested sine parametric model + # Expect to models to share the same parameter obj + model, submodel = nested_sine_model + assert model.parameters["main.sub.a"] is submodel.parameters["sub.a"] + + +@pytest.mark.parametrize( + "A, a, x, expected", + [ + (1.0, 1.0, numpy.pi / 2, 1.0), + (2.0, 2.0, numpy.pi / 4, 2.0), + (1.0, 1.0, numpy.pi, 0.0), + ], +) +def test_parametric_model_evaluation(nested_sine_model, A, a, x, expected): + # C1: Create a nested sine parametric model + # Expect the model to evaluate correctly + model, submodel = nested_sine_model + model.parameters["main.A"].value = A + submodel.parameters["sub.a"].value = a + submodel.parameters["sub.x"].value = x + actual = model.evaluate() + assert numpy.isclose(actual, expected, rtol=1e-6) + + +def test_parametric_pdf_model_parameters(): + # C1: Create a ParametricModelPDF for Ni + # Expect the model to be initialized correctly with all parameters + profile_path = "tests/data/Ni.gr" + profile = Profile() + parser = PDFParser() + parser.parse_file(profile_path) + profile.load_parsed_data(parser) + profile.set_calculation_range(xmax=20) + stru = Structure() + structure_path = "tests/data/Ni.cif" + stru.read(structure_path) + pdf_model = ParametricModelPDF("ni", structure=stru) + parameter_names = [ + "ni.phase.lattice.a", + "ni.phase.Ni0.Uiso", + "ni.delta1", + "ni.delta2", + "ni.qbroad", + "ni.scale", + "ni.qdamp", + "ni.phase.Ni0.occ", + "ni.phase.Ni1.occ", + "ni.phase.Ni2.occ", + "ni.phase.Ni3.occ", + ] + assert set(parameter_names).issubset(set(pdf_model.parameters.keys())) + pdf_model.constrain_symmetry("Fm-3m") + assert set(parameter_names) == set(pdf_model.independent_parameters.keys()) diff --git a/tests/test_pdfadapter.py b/tests/test_pdfadapter.py index 3ebdc75..5632f90 100644 --- a/tests/test_pdfadapter.py +++ b/tests/test_pdfadapter.py @@ -1,13 +1,12 @@ from pathlib import Path import numpy -from helper import make_cmi_recipe from scipy.optimize import least_squares from diffpy.apps.pdfadapter import PDFAdapter -def test_pdfadapter(): +def test_pdfadapter(ni_refined_parameters): # C1: Run the same fit with pdfadapter and diffpy_cmi # Expect the refined parameters to be the same within 1e-5 # diffpy_cmi fitting @@ -29,23 +28,6 @@ def test_pdfadapter(): "qdamp", "qbroad", ] - diffpycmi_recipe = make_cmi_recipe( - str(structure_path), str(profile_path), initial_pv_dict - ) - diffpycmi_recipe.fithooks[0].verbose = 0 - diffpycmi_recipe.fix("all") - - for var_name in variables_to_refine: - diffpycmi_recipe.free(var_name) - least_squares( - diffpycmi_recipe.residual, - diffpycmi_recipe.values, - x_scale="jac", - ) - diffpy_pv_dict = {} - for pname, parameter in diffpycmi_recipe._parameters.items(): - diffpy_pv_dict[pname] = parameter.value - # pdfadapter fitting adapter = PDFAdapter() adapter.initialize_profile( str(profile_path), q_range=(0.1, 25), calculation_range=(1.5, 50, 0.01) @@ -64,5 +46,5 @@ def test_pdfadapter(): pdfadapter_pv_dict[pname] = parameter.value for var_name in variables_to_refine: pdfadapter_value = pdfadapter_pv_dict[var_name] - diffpy_value = diffpy_pv_dict[var_name] + diffpy_value = ni_refined_parameters[var_name] assert numpy.isclose(diffpy_value, pdfadapter_value, atol=1e-5) diff --git a/tests/test_refinement_server.py b/tests/test_refinement_server.py new file mode 100644 index 0000000..2e3537f --- /dev/null +++ b/tests/test_refinement_server.py @@ -0,0 +1,57 @@ +import numpy +import pytest +from mcp import Client + +from diffpy.apps.refinebase.refinement_server import mcp + + +@pytest.fixture +def anyio_backend(): + return "asyncio" + + +@pytest.mark.anyio +async def test_refine_sine(): + # C1: Set up the MCP client and do a sine refinement + # Expect all objs are created and refined successfully + from diffpy.apps.refinebase.refinement_server import session + + async with Client(mcp, raise_exceptions=True) as mcp_client: + await mcp_client.call_tool( + "add_dat_profile", + { + "profile_name": "sine_profile", + "profile_path": "tests/data/sine.dat", + }, + ) + assert "sine_profile" in session.profiles_dict + await mcp_client.call_tool( + "add_equation_model", + { + "model_name": "sine_model", + "equation": "A*sin(x)", + }, + ) + assert "sine_model" in session.models_dict + await mcp_client.call_tool( + "set_model_param_value", + { + "param_name": "sine_model.A", + "value": 0.8, + }, + ) + variable_A = session.get_variable("sine_model.A") + expected_value = 0.8 + actual_value = variable_A.value + assert actual_value == expected_value + await mcp_client.call_tool( + "refine", + { + "profile_names": ["sine_profile"], + "model_names": ["sine_model"], + "variable_names": ["sine_model.A"], + }, + ) + expected_value = 1.0 # The expected value of A after refinement + actual_value = variable_A.value + assert numpy.isclose(actual_value, expected_value, rtol=0.2) diff --git a/tests/test_refinement_session.py b/tests/test_refinement_session.py new file mode 100644 index 0000000..04f185c --- /dev/null +++ b/tests/test_refinement_session.py @@ -0,0 +1,76 @@ +import numpy + +from diffpy.apps.refinebase.parametric_model import ( + ParametricModelEquation, +) +from diffpy.apps.refinebase.refinement_session import RefinementSession + + +def test_refine_sine(nested_sine_model, sine_profile): + # C1: Refinement session without additional calculator or functions + sine_model, submodel = nested_sine_model + session = RefinementSession() + session._solve( + profiles=[sine_profile], + models=[sine_model], + variables=[ + sine_model.parameters["main.A"], + sine_model.parameters["main.sub.a"], + ], + initial_values=[0.8, 0.5], + ) + assert numpy.isclose( + sine_model.parameters["main.A"].value, + 1.0, + rtol=1e-2, + ) + assert numpy.isclose( + sine_model.parameters["main.sub.a"].value, + 1.0, + rtol=1e-2, + ) + + +def test_refine_ni(ni_pdf_model, ni_pdf_profile, ni_refined_parameters): + # C1: Refine Ni example + # Expect the refined parameters are close to the ones + # obtained using diffpy.srfit script + ni_pdf_model.process_meta_data(ni_pdf_profile.meta) + ni_pdf_model.constrain_symmetry("Fm-3m") + ni_model = ParametricModelEquation(name="ni", equation_str="s*pdf") + ni_model.register_submodel(ni_pdf_model, symbol="pdf") + session = RefinementSession() + session._solve( + profiles=[ni_pdf_profile], + models=[ni_model], + variables=[ + ni_pdf_model.parameters["pdf.phase.lattice.a"], + ni_model.parameters["ni.s"], + ni_pdf_model.parameters["pdf.phase.Ni0.Uiso"], + ni_pdf_model.parameters["pdf.delta2"], + ni_pdf_model.parameters["pdf.qdamp"], + ni_pdf_model.parameters["pdf.qbroad"], + ], + initial_values=[ + 3.52, + 0.4, + 0.005, + 2, + 0.04, + 0.02, + ], + ) + name_to_cmi_name = { + "ni.s": "s0", + "ni.pdf.phase.lattice.a": "G1_a", + "ni.pdf.phase.Ni0.Uiso": "G1_Uiso_0", + "ni.pdf.delta2": "G1_delta2", + "ni.pdf.qdamp": "qdamp", + "ni.pdf.qbroad": "qbroad", + } + for name, cmi_name in name_to_cmi_name.items(): + assert numpy.isclose( + ni_model.parameters[name].value, + ni_refined_parameters[cmi_name], + rtol=1e-2, + )