From ec7ab8cd49ddfa2a91b68d619a5e0ffb162f13e8 Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Wed, 5 Aug 2026 18:14:40 -0400 Subject: [PATCH 1/8] refactor: use a more flexible interface to utilize `diffpy.srfit` --- news/refinement.rst | 23 +++++++++ src/diffpy/apps/refinebase/__init__.py | 0 src/diffpy/apps/refinebase/prasers.py | 41 ++++++++++++++++ .../apps/refinebase/refinement_session.py | 47 +++++++++++++++++++ tests/test_refinebase.py | 27 +++++++++++ 5 files changed, 138 insertions(+) create mode 100644 news/refinement.rst create mode 100644 src/diffpy/apps/refinebase/__init__.py create mode 100644 src/diffpy/apps/refinebase/prasers.py create mode 100644 src/diffpy/apps/refinebase/refinement_session.py create mode 100644 tests/test_refinebase.py 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/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/prasers.py b/src/diffpy/apps/refinebase/prasers.py new file mode 100644 index 0000000..58cef4e --- /dev/null +++ b/src/diffpy/apps/refinebase/prasers.py @@ -0,0 +1,41 @@ +class ProfileParser: + """ + A ProfileParser class contains methods to parse profile files and + generate parameters for refinement. + """ + + pass + + +class StructureParser: + """ + A StructureParser class contains methods to parse structure files and + generate parameters for refinement. + """ + + pass + + +class ParameterAdapter: + """ + A ParameterAdapter class contains methods to adapt parameters of + variables format to the standard format used in diffpy + """ + + def __init__(self): + pass + + def _adapt_string(self, parameter: str): + pass + + def _adapt_code(self, function_code: str): + pass + + def _adapt_special(self, special_parameter: str): + pass + + def _adapt_structure_file(self, phase_file: str): + pass + + def _adapt_profile_file(self, profile_file: str): + pass diff --git a/src/diffpy/apps/refinebase/refinement_session.py b/src/diffpy/apps/refinebase/refinement_session.py new file mode 100644 index 0000000..1526580 --- /dev/null +++ b/src/diffpy/apps/refinebase/refinement_session.py @@ -0,0 +1,47 @@ +from diffpy.srfit.fitbase.parameter import ParameterSet + + +class RefinementSession(ParameterSet): + """ + A refinement session class that manages the refinement process. + + Attributes + ---------- + variables : list of Variable + The list of variables in the refinement session. + loss_functions : list of callable + The list of loss functions in the refinement session. + loss_function : callable + The loss function for the refinement session. + + Methods + ------- + register_loss_function() + Register the loss function for refinement. + register_master_loss_function() + Register the master loss function for refinement. + refine() + Perform the refinement. + """ + + def __init__(self): + self.loss_functions = [] + self.loss_function = None + + def _prepare_variables(self): + """Prepare variables for refinement. + + Convert the literals into builders before make the equation""" + pass + + def register_loss_function(self): + """Register the loss function for refinement.""" + pass + + def set_master_loss_function(self, name): + """set the master loss function for refinement.""" + self.master_loss_function = name + + def refine(self): + """Perform the refinement.""" + pass diff --git a/tests/test_refinebase.py b/tests/test_refinebase.py new file mode 100644 index 0000000..44cfb3e --- /dev/null +++ b/tests/test_refinebase.py @@ -0,0 +1,27 @@ +from diffpy.srreal.pdfcalculator import PDFCalculator +from diffpy.apps.refinebase.refinement_session import RefinementSession +from diffpy.apps.refinebase.parsers import ( + ProfileParser, + StructureParser, + ParameterAdapter, +) + + +def test_refinement_session(): + profile_path = "tests/data/Ni.gr" + structure_path = "tests/data/Ni.cif" + profile_parset = ProfileParser(profile_path) + structure_parset = StructureParser(structure_path) + + session = RefinementSession() + session.addParameterSet(profile_parset) + session.addParameterSet(structure_parset) + session.addParameter( + ParameterAdapter( + name="g1", callable=lambda: PDFCalculator(structure_parset)[1] + ) + ) + session.addParameter(ParameterAdapter(name="s1", value=1.0)) + session.register_loss_function(name="l1", expression="s1*g1") + session.set_master_loss_function(name="l1") + session.refine() From 460d1df64798c226966b78d9af530a7026fff465 Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Sat, 8 Aug 2026 18:19:58 -0400 Subject: [PATCH 2/8] feat: implement parsers for profile and phase --- src/diffpy/apps/refinebase/parsers.py | 102 ++++++++++++++++++ src/diffpy/apps/refinebase/prasers.py | 41 ------- .../apps/refinebase/refinement_session.py | 2 +- tests/refinebase/test_parsers.py | 63 +++++++++++ .../test_refinement_session.py} | 12 +-- 5 files changed, 172 insertions(+), 48 deletions(-) create mode 100644 src/diffpy/apps/refinebase/parsers.py delete mode 100644 src/diffpy/apps/refinebase/prasers.py create mode 100644 tests/refinebase/test_parsers.py rename tests/{test_refinebase.py => refinebase/test_refinement_session.py} (80%) diff --git a/src/diffpy/apps/refinebase/parsers.py b/src/diffpy/apps/refinebase/parsers.py new file mode 100644 index 0000000..1e9ca6b --- /dev/null +++ b/src/diffpy/apps/refinebase/parsers.py @@ -0,0 +1,102 @@ +from pathlib import Path + +from diffpy.srfit.fitbase.parameter import Parameter +from diffpy.srfit.fitbase.parameterset import ParameterSet +from diffpy.srfit.pdf.pdfparser import PDFParser +from diffpy.srfit.structure import struToParameterSet +from diffpy.structure import Structure + + +class ParameterParser: + def __init__(self): + pass + + MISSING_FILE_WARNING = ( + "{description} file '{file}' is missing. " + "Please check the path and try again." + ) + + def _parse_pdf( + self, profile_file: str, parset_name: str = "profile_parameterset" + ): + if not Path(profile_file).exists(): + raise FileNotFoundError( + self.MISSING_FILE_WARNING.format( + description="Profile", file=profile_file + ) + ) + pass + parser = PDFParser() + parser.parseFile(profile_file) + parameter_values = parser.getData() + parameter_names = ["x", "y", "dx", "dy"] + parameter_set = _construct_parameterset( + parameter_names, parameter_values, parset_name + ) + meta = dict(parser.getMetaData()) + return parameter_set, meta + + def _parse_structure(self, structure_file: str): + if not Path(structure_file).exists(): + raise FileNotFoundError( + self.MISSING_FILE_WARNING.format( + description="Structure", file=structure_file + ) + ) + stru = Structure() + stru.read(structure_file) + structure_parameterset = struToParameterSet( + "structure_parameterset", stru + ) + return structure_parameterset + + +class ParameterAdapter: + """ + A ParameterAdapter class contains methods to adapt parameters of + variables format to the standard format used in diffpy + """ + + def __init__(self): + pass + + def _adapt_string(self, parameter: str): + pass + + def _adapt_code(self, function_code: str): + pass + + def _adapt_special(self, special_parameter: str): + pass + + def _adapt_structure_file(self, phase_file: str): + pass + + def _adapt_profile_file(self, profile_file: str): + pass + + +def _construct_parameterset(names, values, parset_name): + """ + Construct a ParameterSet from a list of variables. + + Parameters + ---------- + names : list of str + The names of the variables. + values : list of values + The list of values corresponding to the variable names. + parset_name : str + The name of the ParameterSet. + + Returns + ------- + ParameterSet + The converted ParameterSet. + """ + parameter_set = ParameterSet(name=parset_name) + for name, value in zip(names, values): + parameter = Parameter(name) + parameter.setValue(value) + parameter_set.addParameter(parameter) + return parameter_set diff --git a/src/diffpy/apps/refinebase/prasers.py b/src/diffpy/apps/refinebase/prasers.py deleted file mode 100644 index 58cef4e..0000000 --- a/src/diffpy/apps/refinebase/prasers.py +++ /dev/null @@ -1,41 +0,0 @@ -class ProfileParser: - """ - A ProfileParser class contains methods to parse profile files and - generate parameters for refinement. - """ - - pass - - -class StructureParser: - """ - A StructureParser class contains methods to parse structure files and - generate parameters for refinement. - """ - - pass - - -class ParameterAdapter: - """ - A ParameterAdapter class contains methods to adapt parameters of - variables format to the standard format used in diffpy - """ - - def __init__(self): - pass - - def _adapt_string(self, parameter: str): - pass - - def _adapt_code(self, function_code: str): - pass - - def _adapt_special(self, special_parameter: str): - pass - - def _adapt_structure_file(self, phase_file: str): - pass - - def _adapt_profile_file(self, profile_file: str): - pass diff --git a/src/diffpy/apps/refinebase/refinement_session.py b/src/diffpy/apps/refinebase/refinement_session.py index 1526580..9091c19 100644 --- a/src/diffpy/apps/refinebase/refinement_session.py +++ b/src/diffpy/apps/refinebase/refinement_session.py @@ -1,4 +1,4 @@ -from diffpy.srfit.fitbase.parameter import ParameterSet +from diffpy.srfit.fitbase.parameterset import ParameterSet class RefinementSession(ParameterSet): diff --git a/tests/refinebase/test_parsers.py b/tests/refinebase/test_parsers.py new file mode 100644 index 0000000..9a19bc8 --- /dev/null +++ b/tests/refinebase/test_parsers.py @@ -0,0 +1,63 @@ +from diffpy.apps.refinebase.parsers import ( + ParameterParser, +) + + +def test_parse_pdf(): + # C1: reading a valid .gr file + # Expect return valid ParameterSet and meta data + profile_path = "tests/data/Ni.gr" + parser = ParameterParser() + parameter_set, meta = parser._parse_pdf( + profile_path, parset_name="profile_parameterset" + ) + expected_parameter_names = ["x", "y", "dx", "dy"] + actual_parameter_names = [ + name for name in parameter_set._parameters.keys() + ] + assert set(actual_parameter_names) == set(expected_parameter_names) + expected_meta = { + "stype": "X", + "qmin": 0.5, + "qmax": 25.0, + "filename": "tests/data/Ni.gr", + "bank": 0, + "nbanks": 1, + } + actual_meta = meta + assert actual_meta == expected_meta + + +def test_parse_structure(): + # C1: reading a valid .cif file + # Expect return valid ParameterSet + structure_path = "tests/data/Ni.cif" + parser = ParameterParser() + structure_parameterset = parser._parse_structure(structure_path) + expected_lat_parameter_names = ["a", "b", "c", "alpha", "beta", "gamma"] + actual_lat_parameter_names = [ + name for name in structure_parameterset.getLattice()._parameters.keys() + ] + assert set(actual_lat_parameter_names) == set(expected_lat_parameter_names) + for i, atom in enumerate(structure_parameterset.getScatterers()): + expected_atom_parameter_names = [ + "x", + "y", + "z", + "Uiso", + "Biso", + "occ", + "occupancy", + ] + expected_atom_parameter_names += [ + f"U{j+1}{k+1}" for j in range(3) for k in range(3) + ] + expected_atom_parameter_names += [ + f"B{j+1}{k+1}" for j in range(3) for k in range(3) + ] + actual_atom_parameter_names = [ + name for name in atom._parameters.keys() + ] + assert set(actual_atom_parameter_names) == set( + expected_atom_parameter_names + ) diff --git a/tests/test_refinebase.py b/tests/refinebase/test_refinement_session.py similarity index 80% rename from tests/test_refinebase.py rename to tests/refinebase/test_refinement_session.py index 44cfb3e..dd3876d 100644 --- a/tests/test_refinebase.py +++ b/tests/refinebase/test_refinement_session.py @@ -1,17 +1,17 @@ -from diffpy.srreal.pdfcalculator import PDFCalculator -from diffpy.apps.refinebase.refinement_session import RefinementSession from diffpy.apps.refinebase.parsers import ( - ProfileParser, - StructureParser, ParameterAdapter, + ParameterParser, ) +from diffpy.apps.refinebase.refinement_session import RefinementSession +from diffpy.srreal.pdfcalculator import PDFCalculator def test_refinement_session(): + parser = ParameterParser() profile_path = "tests/data/Ni.gr" structure_path = "tests/data/Ni.cif" - profile_parset = ProfileParser(profile_path) - structure_parset = StructureParser(structure_path) + profile_parset, profile_meta = parser._parse_pdf(profile_path) + structure_parset = parser._parse_structure(structure_path) session = RefinementSession() session.addParameterSet(profile_parset) From 58421dbb291e476bd2d82e199f23e080b7f858ef Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Sat, 8 Aug 2026 23:29:18 -0400 Subject: [PATCH 3/8] feat: implement minimal refinement functionalities --- .../{parsers.py => refinable_model.py} | 61 ++++++++--------- .../apps/refinebase/refinement_session.py | 61 +++++++++++++---- tests/refinebase/test_parsers.py | 63 ----------------- tests/refinebase/test_refinement_session.py | 27 -------- tests/test_parsers.py | 67 +++++++++++++++++++ tests/test_refinement_session.py | 36 ++++++++++ 6 files changed, 181 insertions(+), 134 deletions(-) rename src/diffpy/apps/refinebase/{parsers.py => refinable_model.py} (64%) delete mode 100644 tests/refinebase/test_parsers.py delete mode 100644 tests/refinebase/test_refinement_session.py create mode 100644 tests/test_parsers.py create mode 100644 tests/test_refinement_session.py diff --git a/src/diffpy/apps/refinebase/parsers.py b/src/diffpy/apps/refinebase/refinable_model.py similarity index 64% rename from src/diffpy/apps/refinebase/parsers.py rename to src/diffpy/apps/refinebase/refinable_model.py index 1e9ca6b..8ef8499 100644 --- a/src/diffpy/apps/refinebase/parsers.py +++ b/src/diffpy/apps/refinebase/refinable_model.py @@ -1,5 +1,6 @@ from pathlib import Path +from diffpy.srfit.fitbase import Profile from diffpy.srfit.fitbase.parameter import Parameter from diffpy.srfit.fitbase.parameterset import ParameterSet from diffpy.srfit.pdf.pdfparser import PDFParser @@ -7,6 +8,18 @@ from diffpy.structure import Structure +class RefinenableModel(ParameterSet): + """ + A RefinenableModel instance provides access to both the ParameterSet + and the underlying model object. + """ + + def __init__(self, name): + super().__init__(name=name) + self.model = None + self.parameters = None + + class ParameterParser: def __init__(self): pass @@ -28,15 +41,22 @@ def _parse_pdf( pass parser = PDFParser() parser.parseFile(profile_file) + profile = Profile() + profile.loadParsedData(parser) parameter_values = parser.getData() parameter_names = ["x", "y", "dx", "dy"] parameter_set = _construct_parameterset( parameter_names, parameter_values, parset_name ) + profile_model = RefinenableModel(name=parset_name) + profile_model.model = profile + profile_model.parameters = parameter_set meta = dict(parser.getMetaData()) - return parameter_set, meta + return profile_model, meta - def _parse_structure(self, structure_file: str): + def _parse_structure( + self, structure_file: str, parset_name: str = "structure_parameterset" + ): if not Path(structure_file).exists(): raise FileNotFoundError( self.MISSING_FILE_WARNING.format( @@ -45,35 +65,16 @@ def _parse_structure(self, structure_file: str): ) stru = Structure() stru.read(structure_file) - structure_parameterset = struToParameterSet( - "structure_parameterset", stru + stru_diffpy_parset = struToParameterSet(parset_name, stru) + structure_mdel = RefinenableModel(name=parset_name) + structure_mdel.model = stru + structure_mdel.parameters = ParameterSet(name=parset_name) + structure_mdel.parameters.addParameterSet( + stru_diffpy_parset.getLattice() ) - return structure_parameterset - - -class ParameterAdapter: - """ - A ParameterAdapter class contains methods to adapt parameters of - variables format to the standard format used in diffpy - """ - - def __init__(self): - pass - - def _adapt_string(self, parameter: str): - pass - - def _adapt_code(self, function_code: str): - pass - - def _adapt_special(self, special_parameter: str): - pass - - def _adapt_structure_file(self, phase_file: str): - pass - - def _adapt_profile_file(self, profile_file: str): - pass + for atom_parset in stru_diffpy_parset.getScatterers(): + structure_mdel.parameters.addParameterSet(atom_parset) + return structure_mdel def _construct_parameterset(names, values, parset_name): diff --git a/src/diffpy/apps/refinebase/refinement_session.py b/src/diffpy/apps/refinebase/refinement_session.py index 9091c19..f02a9bf 100644 --- a/src/diffpy/apps/refinebase/refinement_session.py +++ b/src/diffpy/apps/refinebase/refinement_session.py @@ -1,7 +1,12 @@ +from collections import OrderedDict + +from scipy.optimize import leastsq + +from diffpy.srfit.fitbase import FitContribution, FitRecipe, Profile from diffpy.srfit.fitbase.parameterset import ParameterSet -class RefinementSession(ParameterSet): +class RefinementSession: """ A refinement session class that manages the refinement process. @@ -25,23 +30,51 @@ class RefinementSession(ParameterSet): """ def __init__(self): - self.loss_functions = [] - self.loss_function = None + self.main_parameter_set = ParameterSet(name="main_parameter_set") + self.calculators = [] + self.contributions = OrderedDict() + + def add_parameterSet(self, parset): + """Add a ParameterSet to the refinement session.""" + self.main_parameter_set.addParameterSet(parset) - def _prepare_variables(self): - """Prepare variables for refinement. + def add_parameter(self, parameter): + """Add a Parameter to the refinement session.""" + self.main_parameter_set.addParameter(parameter) - Convert the literals into builders before make the equation""" + def add_function(self, name, expression=None, ns={}): pass - def register_loss_function(self): - """Register the loss function for refinement.""" + def add_calculator(self, calculator): pass - def set_master_loss_function(self, name): - """set the master loss function for refinement.""" - self.master_loss_function = name + def add_profile(self, name=None, profile=None, x=None, y=None, dy=None): + if profile is None: + profile = Profile() + profile.x = x + profile.y = y + if dy is not None: + profile.dy = dy + contribution = FitContribution(name=name) + contribution.setProfile(profile, xname=profile.x.name) + self.contributions[name] = contribution - def refine(self): - """Perform the refinement.""" - pass + def set_profile_equation(self, profile_name, expression): + self.contributions[profile_name].setEquation(expression) + + def set_profile_weights(self, names, weights): + self.recipe = FitRecipe() + for name, weight in zip(names, weights): + self.recipe.addContribution( + self.contributions[name], weight=weight + ) + + def refine(self, var_names, initial_values): + for name, value in zip(var_names, initial_values): + self.main_parameter_set.parameters[name].setValue(value) + for name in var_names: + self.recipe.addVar(self.main_parameter_set.parameters[name]) + self.recipe.fix("all") + for name in var_names: + self.recipe.free(name) + leastsq(self.recipe.residual, self.recipe.values) diff --git a/tests/refinebase/test_parsers.py b/tests/refinebase/test_parsers.py deleted file mode 100644 index 9a19bc8..0000000 --- a/tests/refinebase/test_parsers.py +++ /dev/null @@ -1,63 +0,0 @@ -from diffpy.apps.refinebase.parsers import ( - ParameterParser, -) - - -def test_parse_pdf(): - # C1: reading a valid .gr file - # Expect return valid ParameterSet and meta data - profile_path = "tests/data/Ni.gr" - parser = ParameterParser() - parameter_set, meta = parser._parse_pdf( - profile_path, parset_name="profile_parameterset" - ) - expected_parameter_names = ["x", "y", "dx", "dy"] - actual_parameter_names = [ - name for name in parameter_set._parameters.keys() - ] - assert set(actual_parameter_names) == set(expected_parameter_names) - expected_meta = { - "stype": "X", - "qmin": 0.5, - "qmax": 25.0, - "filename": "tests/data/Ni.gr", - "bank": 0, - "nbanks": 1, - } - actual_meta = meta - assert actual_meta == expected_meta - - -def test_parse_structure(): - # C1: reading a valid .cif file - # Expect return valid ParameterSet - structure_path = "tests/data/Ni.cif" - parser = ParameterParser() - structure_parameterset = parser._parse_structure(structure_path) - expected_lat_parameter_names = ["a", "b", "c", "alpha", "beta", "gamma"] - actual_lat_parameter_names = [ - name for name in structure_parameterset.getLattice()._parameters.keys() - ] - assert set(actual_lat_parameter_names) == set(expected_lat_parameter_names) - for i, atom in enumerate(structure_parameterset.getScatterers()): - expected_atom_parameter_names = [ - "x", - "y", - "z", - "Uiso", - "Biso", - "occ", - "occupancy", - ] - expected_atom_parameter_names += [ - f"U{j+1}{k+1}" for j in range(3) for k in range(3) - ] - expected_atom_parameter_names += [ - f"B{j+1}{k+1}" for j in range(3) for k in range(3) - ] - actual_atom_parameter_names = [ - name for name in atom._parameters.keys() - ] - assert set(actual_atom_parameter_names) == set( - expected_atom_parameter_names - ) diff --git a/tests/refinebase/test_refinement_session.py b/tests/refinebase/test_refinement_session.py deleted file mode 100644 index dd3876d..0000000 --- a/tests/refinebase/test_refinement_session.py +++ /dev/null @@ -1,27 +0,0 @@ -from diffpy.apps.refinebase.parsers import ( - ParameterAdapter, - ParameterParser, -) -from diffpy.apps.refinebase.refinement_session import RefinementSession -from diffpy.srreal.pdfcalculator import PDFCalculator - - -def test_refinement_session(): - parser = ParameterParser() - profile_path = "tests/data/Ni.gr" - structure_path = "tests/data/Ni.cif" - profile_parset, profile_meta = parser._parse_pdf(profile_path) - structure_parset = parser._parse_structure(structure_path) - - session = RefinementSession() - session.addParameterSet(profile_parset) - session.addParameterSet(structure_parset) - session.addParameter( - ParameterAdapter( - name="g1", callable=lambda: PDFCalculator(structure_parset)[1] - ) - ) - session.addParameter(ParameterAdapter(name="s1", value=1.0)) - session.register_loss_function(name="l1", expression="s1*g1") - session.set_master_loss_function(name="l1") - session.refine() diff --git a/tests/test_parsers.py b/tests/test_parsers.py new file mode 100644 index 0000000..074426f --- /dev/null +++ b/tests/test_parsers.py @@ -0,0 +1,67 @@ +from diffpy.apps.refinebase.refinable_model import ( + ParameterParser, +) + + +def test_parse_pdf(): + # C1: reading a valid .gr file + # Expect return valid ParameterSet and meta data + profile_path = "tests/data/Ni.gr" + parser = ParameterParser() + profile_model, meta = parser._parse_pdf( + profile_path, parset_name="profile_parameterset" + ) + expected_parameter_names = ["x", "y", "dx", "dy"] + actual_parameter_names = [par.name for par in profile_model.parameters] + assert set(actual_parameter_names) == set(expected_parameter_names) + expected_meta = { + "stype": "X", + "qmin": 0.5, + "qmax": 25.0, + "filename": "tests/data/Ni.gr", + "bank": 0, + "nbanks": 1, + } + actual_meta = meta + assert actual_meta == expected_meta + + +def test_parse_structure(): + # C1: reading a valid .cif file + # Expect return valid ParameterSet + structure_path = "tests/data/Ni.cif" + parser = ParameterParser() + structure_model = parser._parse_structure(structure_path) + expected_parameter_names = ["a", "b", "c", "alpha", "beta", "gamma"] + actual_parameter_names = [ + name + for name in structure_model.parameters._parsets[ + "lattice" + ]._parameters.keys() + ] + assert set(actual_parameter_names) == set(expected_parameter_names) + expected_atom_parameter_names = [ + "x", + "y", + "z", + "Uiso", + "Biso", + "occ", + "occupancy", + ] + expected_atom_parameter_names += [ + f"U{j+1}{k+1}" for j in range(3) for k in range(3) + ] + expected_atom_parameter_names += [ + f"B{j+1}{k+1}" for j in range(3) for k in range(3) + ] + for i in range(len(structure_model.model)): + actual_atom_parameter_names = [ + name + for name in structure_model.parameters._parsets[ + f"Ni{i}" + ]._parameters.keys() + ] + assert set(actual_atom_parameter_names) == set( + expected_atom_parameter_names + ) diff --git a/tests/test_refinement_session.py b/tests/test_refinement_session.py new file mode 100644 index 0000000..cd17e26 --- /dev/null +++ b/tests/test_refinement_session.py @@ -0,0 +1,36 @@ +import numpy + +from diffpy.apps.refinebase.refinement_session import RefinementSession + + +def test_refinement_session(): + # C1: Refinement session without additional calculator or functions + session = RefinementSession() + xobs = numpy.linspace(-numpy.pi, numpy.pi, 100) + yobs = numpy.sin(xobs) + 0.1 * numpy.random.normal(size=xobs.shape) + session.add_profile(name="profile", x=xobs, y=yobs) + session.add_parameter(name="x", delegates_to="profile.x") + session.set_profile_equation(profile="profile", expression="A*sin(a*x)") + session.set_profile_weights(["profile"], [1.0]) + session.refine(name=["A", "a"], initial_values=[1.0, 0.5]) + + # C2: Refinement session with one PDFCalculator + # parser = ParameterParser() + # profile_path = "tests/data/Ni.gr" + # structure_path = "tests/data/Ni.cif" + # profile_model, profile_meta = parser._parse_pdf(profile_path) + # structure_model = parser._parse_structure(structure_path) + + # session = RefinementSession() + # session.addParameterSet(profile_model.parameters) + # session.addParameterSet(structure_model.parameters) + # session.addParameter( + # ParameterAdapter( + # name="g1", + # callable=lambda: PDFCalculator(structure_model.model)[1] + # ) + # ) + # session.addParameter(ParameterAdapter(name="s1", value=1.0)) + # session.register_loss_function(name="l1", expression="s1*g1") + # session.set_master_loss_function(name="l1") + # session.refine() From 35622091bd45a69007af6c663736c1c3f5196675 Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Sun, 9 Aug 2026 22:37:40 -0400 Subject: [PATCH 4/8] feat: add ParameterSetTree and implement the refinement interface --- src/diffpy/apps/refinebase/refinable_model.py | 42 ++++++++-- .../apps/refinebase/refinement_session.py | 78 +++++++++++++------ ...est_parsers.py => test_refinable_model.py} | 24 +++++- tests/test_refinement_session.py | 24 ++++-- 4 files changed, 130 insertions(+), 38 deletions(-) rename tests/{test_parsers.py => test_refinable_model.py} (68%) diff --git a/src/diffpy/apps/refinebase/refinable_model.py b/src/diffpy/apps/refinebase/refinable_model.py index 8ef8499..3626cbe 100644 --- a/src/diffpy/apps/refinebase/refinable_model.py +++ b/src/diffpy/apps/refinebase/refinable_model.py @@ -1,5 +1,7 @@ from pathlib import Path +import networkx as nx + from diffpy.srfit.fitbase import Profile from diffpy.srfit.fitbase.parameter import Parameter from diffpy.srfit.fitbase.parameterset import ParameterSet @@ -20,6 +22,30 @@ def __init__(self, name): self.parameters = None +class ParameterSetTree: + """ + A TreeView instance provides a tree view of the ParameterSet. + """ + + def __init__(self, parameter_set): + self.graph = nx.DiGraph() + self._construct_parameter_tree_view(parameter_set, prefix="") + + def _construct_parameter_tree_view(self, parameterset, prefix=""): + parent_name = f"{prefix}{parameterset.name}" + self.graph.add_node(parent_name, parameter=parameterset) + + for par in parameterset._iterManaged(): + child_name = f"{parent_name}.{par.name}" + self.graph.add_node(child_name, parameter=par) + self.graph.add_edge(parent_name, child_name) + if hasattr(par, "_iterManaged"): + self._construct_parameter_tree_view( + par, + prefix=f"{parent_name}.", + ) + + class ParameterParser: def __init__(self): pass @@ -45,7 +71,7 @@ def _parse_pdf( profile.loadParsedData(parser) parameter_values = parser.getData() parameter_names = ["x", "y", "dx", "dy"] - parameter_set = _construct_parameterset( + parameter_set = _create_parameter_set( parameter_names, parameter_values, parset_name ) profile_model = RefinenableModel(name=parset_name) @@ -66,18 +92,18 @@ def _parse_structure( stru = Structure() stru.read(structure_file) stru_diffpy_parset = struToParameterSet(parset_name, stru) - structure_mdel = RefinenableModel(name=parset_name) - structure_mdel.model = stru - structure_mdel.parameters = ParameterSet(name=parset_name) - structure_mdel.parameters.addParameterSet( + structure_model = RefinenableModel(name=parset_name) + structure_model.model = stru + structure_model.parameters = ParameterSet(name=parset_name) + structure_model.parameters.addParameterSet( stru_diffpy_parset.getLattice() ) for atom_parset in stru_diffpy_parset.getScatterers(): - structure_mdel.parameters.addParameterSet(atom_parset) - return structure_mdel + structure_model.parameters.addParameterSet(atom_parset) + return structure_model -def _construct_parameterset(names, values, parset_name): +def _create_parameter_set(names, values, parset_name): """ Construct a ParameterSet from a list of variables. diff --git a/src/diffpy/apps/refinebase/refinement_session.py b/src/diffpy/apps/refinebase/refinement_session.py index f02a9bf..22a5b7d 100644 --- a/src/diffpy/apps/refinebase/refinement_session.py +++ b/src/diffpy/apps/refinebase/refinement_session.py @@ -1,7 +1,9 @@ +import uuid from collections import OrderedDict from scipy.optimize import leastsq +from diffpy.apps.refinebase.refinable_model import ParameterSetTree from diffpy.srfit.fitbase import FitContribution, FitRecipe, Profile from diffpy.srfit.fitbase.parameterset import ParameterSet @@ -30,17 +32,18 @@ class RefinementSession: """ def __init__(self): - self.main_parameter_set = ParameterSet(name="main_parameter_set") - self.calculators = [] + self.main_parameter_set = ParameterSet(name="main") + self.calculators = OrderedDict() self.contributions = OrderedDict() + self.recipes = OrderedDict() def add_parameterSet(self, parset): """Add a ParameterSet to the refinement session.""" self.main_parameter_set.addParameterSet(parset) - def add_parameter(self, parameter): + def add_parameter(self, parameter=None, name=None, delegates_to=None): """Add a Parameter to the refinement session.""" - self.main_parameter_set.addParameter(parameter) + self.main_parameter_set.addParameterSet(parameter) def add_function(self, name, expression=None, ns={}): pass @@ -48,7 +51,16 @@ def add_function(self, name, expression=None, ns={}): def add_calculator(self, calculator): pass - def add_profile(self, name=None, profile=None, x=None, y=None, dy=None): + def add_case( + self, + name=None, + profile=None, + x=None, + y=None, + dy=None, + expression=None, + xname=None, + ): if profile is None: profile = Profile() profile.x = x @@ -56,25 +68,45 @@ def add_profile(self, name=None, profile=None, x=None, y=None, dy=None): if dy is not None: profile.dy = dy contribution = FitContribution(name=name) - contribution.setProfile(profile, xname=profile.x.name) + contribution.setProfile(profile, xname=xname) + contribution.setEquation(expression) + self.main_parameter_set.addParameterSet(contribution) self.contributions[name] = contribution - def set_profile_equation(self, profile_name, expression): - self.contributions[profile_name].setEquation(expression) - - def set_profile_weights(self, names, weights): - self.recipe = FitRecipe() - for name, weight in zip(names, weights): - self.recipe.addContribution( - self.contributions[name], weight=weight + def refine( + self, + case_names, + case_weights, + var_names, + initial_values, + id=uuid.uuid4(), + ): + # Initialize the recipe + recipe = FitRecipe() + self.recipes[id] = recipe + for name, weight in zip(case_names, case_weights): + recipe.addContribution(self.contributions[name], weight=weight) + # Add variables + temp_parameter_tree = ParameterSetTree(self.main_parameter_set) + for i, name in enumerate(var_names): + recipe.addVar( + temp_parameter_tree.graph.nodes[name]["parameter"], + value=initial_values[i], + name=name, ) - - def refine(self, var_names, initial_values): - for name, value in zip(var_names, initial_values): - self.main_parameter_set.parameters[name].setValue(value) - for name in var_names: - self.recipe.addVar(self.main_parameter_set.parameters[name]) - self.recipe.fix("all") + # Refine the recipe + recipe.fix("all") for name in var_names: - self.recipe.free(name) - leastsq(self.recipe.residual, self.recipe.values) + recipe.free(name) + leastsq(recipe.residual, recipe.values) + + def clear(self, level=None): + if level is None: + level = 1 + + if level <= 1: + self.main_parameter_set = ParameterSet(name="main") + if level <= 2: + self.contributions = OrderedDict() + if level <= 3: + self.recipe = OrderedDict() diff --git a/tests/test_parsers.py b/tests/test_refinable_model.py similarity index 68% rename from tests/test_parsers.py rename to tests/test_refinable_model.py index 074426f..5c5ae48 100644 --- a/tests/test_parsers.py +++ b/tests/test_refinable_model.py @@ -1,5 +1,6 @@ from diffpy.apps.refinebase.refinable_model import ( ParameterParser, + ParameterSetTree, ) @@ -12,7 +13,9 @@ def test_parse_pdf(): profile_path, parset_name="profile_parameterset" ) expected_parameter_names = ["x", "y", "dx", "dy"] - actual_parameter_names = [par.name for par in profile_model.parameters] + actual_parameter_names = [ + par.name for par in profile_model.parameters.iterPars() + ] assert set(actual_parameter_names) == set(expected_parameter_names) expected_meta = { "stype": "X", @@ -65,3 +68,22 @@ def test_parse_structure(): assert set(actual_atom_parameter_names) == set( expected_atom_parameter_names ) + + +def test_parameter_tree(): + # C1: Check if the parameter tree is constructed correctly + # Expect the tree to have the corresponding nodes and keeps + # the same structure as the ParameterSet + structure_path = "tests/data/Ni.cif" + parser = ParameterParser() + structure_model = parser._parse_structure( + structure_path, parset_name="phase" + ) + parameters = structure_model.parameters + tree_view = ParameterSetTree(parameters) + expected_nodes = set( + ["phase.lattice", "phase.lattice.a", "phase.Ni0", "phase.Ni0.x"] + ) + assert expected_nodes.issubset(set(tree_view.graph.nodes)) + assert tree_view.graph.has_edge("phase.lattice", "phase.lattice.a") + assert tree_view.graph.has_edge("phase.Ni0", "phase.Ni0.x") diff --git a/tests/test_refinement_session.py b/tests/test_refinement_session.py index cd17e26..d950c4a 100644 --- a/tests/test_refinement_session.py +++ b/tests/test_refinement_session.py @@ -1,5 +1,6 @@ import numpy +from diffpy.apps.refinebase.refinable_model import ParameterSetTree from diffpy.apps.refinebase.refinement_session import RefinementSession @@ -7,12 +8,23 @@ def test_refinement_session(): # C1: Refinement session without additional calculator or functions session = RefinementSession() xobs = numpy.linspace(-numpy.pi, numpy.pi, 100) - yobs = numpy.sin(xobs) + 0.1 * numpy.random.normal(size=xobs.shape) - session.add_profile(name="profile", x=xobs, y=yobs) - session.add_parameter(name="x", delegates_to="profile.x") - session.set_profile_equation(profile="profile", expression="A*sin(a*x)") - session.set_profile_weights(["profile"], [1.0]) - session.refine(name=["A", "a"], initial_values=[1.0, 0.5]) + yobs = 2 * numpy.sin(2 * xobs) + 0.1 * numpy.random.normal(size=xobs.shape) + session.add_case( + name="sine_fit", + x=xobs, + y=yobs, + expression="A*sin(a*x)", + xname="x", + ) + session.refine( + case_names=["sine_fit"], + case_weights=[1.0], + var_names=["main.sine_fit.A", "main.sine_fit.a"], + initial_values=[1.0, 1.0], + ) + tree = ParameterSetTree(session.main_parameter_set).graph + assert numpy.isclose(tree.nodes["main.sine_fit.A"]["parameter"].value, 1.0) + assert numpy.isclose(tree.nodes["main.sine_fit.a"]["parameter"].value, 1.0) # C2: Refinement session with one PDFCalculator # parser = ParameterParser() From 9f61c05f870245c50fafe12f503163b1506fd09d Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Mon, 10 Aug 2026 13:03:26 -0400 Subject: [PATCH 5/8] chore: add dependencies --- requirements/conda.txt | 1 + src/diffpy/apps/refinebase/refinement_session.py | 2 +- tests/test_refinement_session.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/requirements/conda.txt b/requirements/conda.txt index 61d392d..e395a4e 100644 --- a/requirements/conda.txt +++ b/requirements/conda.txt @@ -6,3 +6,4 @@ pyyaml diffpy.srfit diffpy.srreal diffpy.structure +networkx diff --git a/src/diffpy/apps/refinebase/refinement_session.py b/src/diffpy/apps/refinebase/refinement_session.py index 22a5b7d..f65da35 100644 --- a/src/diffpy/apps/refinebase/refinement_session.py +++ b/src/diffpy/apps/refinebase/refinement_session.py @@ -73,7 +73,7 @@ def add_case( self.main_parameter_set.addParameterSet(contribution) self.contributions[name] = contribution - def refine( + def solve( self, case_names, case_weights, diff --git a/tests/test_refinement_session.py b/tests/test_refinement_session.py index d950c4a..7314cc6 100644 --- a/tests/test_refinement_session.py +++ b/tests/test_refinement_session.py @@ -16,7 +16,7 @@ def test_refinement_session(): expression="A*sin(a*x)", xname="x", ) - session.refine( + session.solve( case_names=["sine_fit"], case_weights=[1.0], var_names=["main.sine_fit.A", "main.sine_fit.a"], From 444e3687434f02d2415d8ba95679d92507d969f0 Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Mon, 10 Aug 2026 13:29:57 -0400 Subject: [PATCH 6/8] chore: update test data to ensure refinement convergence --- src/diffpy/apps/refinebase/refinement_session.py | 15 +++++++++------ tests/test_refinement_session.py | 12 ++++++++---- 2 files changed, 17 insertions(+), 10 deletions(-) diff --git a/src/diffpy/apps/refinebase/refinement_session.py b/src/diffpy/apps/refinebase/refinement_session.py index f65da35..d828c92 100644 --- a/src/diffpy/apps/refinebase/refinement_session.py +++ b/src/diffpy/apps/refinebase/refinement_session.py @@ -63,10 +63,7 @@ def add_case( ): if profile is None: profile = Profile() - profile.x = x - profile.y = y - if dy is not None: - profile.dy = dy + profile.setObservedProfile(x, y, dy) contribution = FitContribution(name=name) contribution.setProfile(profile, xname=xname) contribution.setEquation(expression) @@ -88,15 +85,21 @@ def solve( recipe.addContribution(self.contributions[name], weight=weight) # Add variables temp_parameter_tree = ParameterSetTree(self.main_parameter_set) + for name in var_names: + if name not in temp_parameter_tree.graph.nodes: + raise ValueError( + f"Variable '{name}' not found in the parameter tree. " + ) + recipe_var_names = [name.replace(".", "_") for name in var_names] for i, name in enumerate(var_names): recipe.addVar( temp_parameter_tree.graph.nodes[name]["parameter"], value=initial_values[i], - name=name, + name=recipe_var_names[i], ) # Refine the recipe recipe.fix("all") - for name in var_names: + for name in recipe_var_names: recipe.free(name) leastsq(recipe.residual, recipe.values) diff --git a/tests/test_refinement_session.py b/tests/test_refinement_session.py index 7314cc6..b315237 100644 --- a/tests/test_refinement_session.py +++ b/tests/test_refinement_session.py @@ -8,7 +8,7 @@ def test_refinement_session(): # C1: Refinement session without additional calculator or functions session = RefinementSession() xobs = numpy.linspace(-numpy.pi, numpy.pi, 100) - yobs = 2 * numpy.sin(2 * xobs) + 0.1 * numpy.random.normal(size=xobs.shape) + yobs = numpy.sin(xobs) + 1e-1 * numpy.random.normal(size=xobs.shape) session.add_case( name="sine_fit", x=xobs, @@ -20,11 +20,15 @@ def test_refinement_session(): case_names=["sine_fit"], case_weights=[1.0], var_names=["main.sine_fit.A", "main.sine_fit.a"], - initial_values=[1.0, 1.0], + initial_values=[0.5, 0.5], ) tree = ParameterSetTree(session.main_parameter_set).graph - assert numpy.isclose(tree.nodes["main.sine_fit.A"]["parameter"].value, 1.0) - assert numpy.isclose(tree.nodes["main.sine_fit.a"]["parameter"].value, 1.0) + assert numpy.isclose( + tree.nodes["main.sine_fit.A"]["parameter"].value, 1.0, rtol=1e-2 + ) + assert numpy.isclose( + tree.nodes["main.sine_fit.a"]["parameter"].value, 1.0, rtol=1e-2 + ) # C2: Refinement session with one PDFCalculator # parser = ParameterParser() From 5c65294d7580aa01d6cc39d0b09cb53e0fec049c Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Fri, 14 Aug 2026 19:47:15 -0400 Subject: [PATCH 7/8] feat: add `ParametricModelPDF` --- .codespell/ignore_words.txt | 3 + src/diffpy/__init__.py | 14 -- src/diffpy/apps/pdfadapter.py | 3 +- .../apps/refinebase/parametric_model.py | 141 ++++++++++++++++++ src/diffpy/apps/refinebase/refinable_model.py | 129 ---------------- .../apps/refinebase/refinement_session.py | 117 +++------------ tests/conftest.py | 14 ++ tests/helper.py | 1 - tests/test_parametric_model.py | 48 ++++++ tests/test_refinable_model.py | 89 ----------- tests/test_refinement_session.py | 95 +++++++----- 11 files changed, 287 insertions(+), 367 deletions(-) delete mode 100644 src/diffpy/__init__.py create mode 100644 src/diffpy/apps/refinebase/parametric_model.py delete mode 100644 src/diffpy/apps/refinebase/refinable_model.py create mode 100644 tests/test_parametric_model.py delete mode 100644 tests/test_refinable_model.py 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/src/diffpy/__init__.py b/src/diffpy/__init__.py deleted file mode 100644 index 2d7de20..0000000 --- a/src/diffpy/__init__.py +++ /dev/null @@ -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..dc9b850 100644 --- a/src/diffpy/apps/pdfadapter.py +++ b/src/diffpy/apps/pdfadapter.py @@ -3,8 +3,6 @@ from pathlib import Path import numpy -from scipy.optimize import least_squares - from diffpy.srfit.fitbase import ( FitContribution, FitRecipe, @@ -14,6 +12,7 @@ from diffpy.srfit.pdf import PDFGenerator, PDFParser from diffpy.srfit.structure import constrainAsSpaceGroup from diffpy.structure.parsers import getParser +from scipy.optimize import least_squares class PDFAdapter: diff --git a/src/diffpy/apps/refinebase/parametric_model.py b/src/diffpy/apps/refinebase/parametric_model.py new file mode 100644 index 0000000..6b892b2 --- /dev/null +++ b/src/diffpy/apps/refinebase/parametric_model.py @@ -0,0 +1,141 @@ +from collections import OrderedDict + +import networkx as nx +import numpy +from diffpy.srfit.equation.literals import Operator, makeOperator +from diffpy.srfit.fitbase import FitContribution, Profile +from diffpy.srfit.pdf.pdfgenerator import PDFGenerator +from diffpy.structure import Structure + + +class ParametricModel: + # TODO: add constraints and restraints method + def __init__(self, name): + self.meta = OrderedDict() + self.name = name + self._contribution = FitContribution(name) + self._graph = nx.DiGraph() + # NOTE: all submodels will share the same profile + self._submodels = [] + old_validate = self._contribution._validate + self._contribution._validate = lambda: self.validate(old_validate) + + def validate(self, old_validate): + # must provide placeholders to pass the validation + if self._contribution.profile is None: + placeholder_profile = Profile() + placeholder_profile.setObservedProfile( + numpy.arange(100), numpy.arange(100) + ) + self.set_profile(placeholder_profile) + if self._contribution._eq is None: + placeholder_equation = makeOperator( + name=self.pdf_generator.name, + symbol="g", + operation=self.pdf_generator.operation, + nin=0, # NOTE: make sure nin=0 and nout=1 is correct + nout=1, + ) + self._contribution.setEquation("g", ns={"g": placeholder_equation}) + old_validate() + + def _construct_parameter_graph(self, parameterset, prefix=""): + parent_name = f"{prefix}{parameterset.name}" + self._graph.add_node(parent_name, parameter=parameterset) + + for par in parameterset._iterManaged(): + child_name = f"{parent_name}.{par.name}" + self._graph.add_node(child_name, parameter=par) + self._graph.add_edge(parent_name, child_name) + if hasattr(par, "_iterManaged"): + self._construct_parameter_graph( + par, + prefix=f"{parent_name}.", + ) + + def register_submodel(self, symbol, model): + """Register a parametric model to the current model.""" + op = model.evaluate + if not isinstance(model.evaluate, Operator): + op = makeOperator( + name=model.name, + symbol=symbol, + operation=model.evaluate, + nin=0, # NOTE: make sure nin=0 and nout=1 is correct + nout=1, + ) + self._contribution._eqfactory.registerOperator(symbol, op) + # Allow iterPars to traverse the submodel's parameters + self._contribution.addParameterSet(model._contribution) + self._submodels.append(model) + + @property + def parameters(self): + return { + par_node_id: self._graph.nodes[par_node_id]["parameter"] + for par_node_id in self._graph.nodes + if self._graph.out_degree(par_node_id) == 0 + } + + def set_profile(self, profile): + self._contribution.setProfile(profile) + if hasattr(self, "pdf_generator"): + self.pdf_generator.setProfile(profile) + for submodel in self._submodels: + submodel.set_profile(profile) + + def set_equation(self, equation_str, ns={}): + self._contribution.setEquation(equation_str, ns=ns) + + def get_equation(self): + return self._contribution.getEquation() + + def add_parameter(self, parameter): + self._contribution.addParameter(parameter) + + def remove_parameter(self, parameter): + self._contribution.removeParameter(parameter) + + def add_parameter_set(self, parameterset): + self._contribution.addParameterSet(parameterset) + + def remove_parameter_set(self, parameterset): + self._contribution.removeParameterSet(parameterset) + + def prepare(self): + self._graph.clear() + self._construct_parameter_graph(self._contribution, prefix="") + + def evaluate(self): + return self._contribution._eq() + + def residual(self): + # TODO: Implement residual calculation for the model + pass + + +class ParametricModelPDF(ParametricModel): + def __init__(self, name, structure: Structure, meta=None): + super().__init__(name=name) + self._initialize_pdf_generator(structure, meta) + + def _initialize_pdf_generator(self, structure: Structure, meta: dict): + self.pdf_generator = PDFGenerator(self.name) + self.pdf_generator.setStructure(structure) + self._contribution.addParameterSet(self.pdf_generator) + self._contribution._RecipeContainer__managed = ( + self.pdf_generator._RecipeContainer__managed + ) + self._contribution._parameters = self.pdf_generator._parameters + + if meta is not None: + self.processMetaData(meta) + + def processMetaData(self, meta: dict = None): + self.pdf_generator.meta.update(meta) + self.pdf_generator.processMetaData() + + def evaluate(self): + if self._contribution.profile is None: + raise ValueError("Profile is not set for the PDF model.") + return self.pdf_generator.operation() diff --git a/src/diffpy/apps/refinebase/refinable_model.py b/src/diffpy/apps/refinebase/refinable_model.py deleted file mode 100644 index 3626cbe..0000000 --- a/src/diffpy/apps/refinebase/refinable_model.py +++ /dev/null @@ -1,129 +0,0 @@ -from pathlib import Path - -import networkx as nx - -from diffpy.srfit.fitbase import Profile -from diffpy.srfit.fitbase.parameter import Parameter -from diffpy.srfit.fitbase.parameterset import ParameterSet -from diffpy.srfit.pdf.pdfparser import PDFParser -from diffpy.srfit.structure import struToParameterSet -from diffpy.structure import Structure - - -class RefinenableModel(ParameterSet): - """ - A RefinenableModel instance provides access to both the ParameterSet - and the underlying model object. - """ - - def __init__(self, name): - super().__init__(name=name) - self.model = None - self.parameters = None - - -class ParameterSetTree: - """ - A TreeView instance provides a tree view of the ParameterSet. - """ - - def __init__(self, parameter_set): - self.graph = nx.DiGraph() - self._construct_parameter_tree_view(parameter_set, prefix="") - - def _construct_parameter_tree_view(self, parameterset, prefix=""): - parent_name = f"{prefix}{parameterset.name}" - self.graph.add_node(parent_name, parameter=parameterset) - - for par in parameterset._iterManaged(): - child_name = f"{parent_name}.{par.name}" - self.graph.add_node(child_name, parameter=par) - self.graph.add_edge(parent_name, child_name) - if hasattr(par, "_iterManaged"): - self._construct_parameter_tree_view( - par, - prefix=f"{parent_name}.", - ) - - -class ParameterParser: - def __init__(self): - pass - - MISSING_FILE_WARNING = ( - "{description} file '{file}' is missing. " - "Please check the path and try again." - ) - - def _parse_pdf( - self, profile_file: str, parset_name: str = "profile_parameterset" - ): - if not Path(profile_file).exists(): - raise FileNotFoundError( - self.MISSING_FILE_WARNING.format( - description="Profile", file=profile_file - ) - ) - pass - parser = PDFParser() - parser.parseFile(profile_file) - profile = Profile() - profile.loadParsedData(parser) - parameter_values = parser.getData() - parameter_names = ["x", "y", "dx", "dy"] - parameter_set = _create_parameter_set( - parameter_names, parameter_values, parset_name - ) - profile_model = RefinenableModel(name=parset_name) - profile_model.model = profile - profile_model.parameters = parameter_set - meta = dict(parser.getMetaData()) - return profile_model, meta - - def _parse_structure( - self, structure_file: str, parset_name: str = "structure_parameterset" - ): - if not Path(structure_file).exists(): - raise FileNotFoundError( - self.MISSING_FILE_WARNING.format( - description="Structure", file=structure_file - ) - ) - stru = Structure() - stru.read(structure_file) - stru_diffpy_parset = struToParameterSet(parset_name, stru) - structure_model = RefinenableModel(name=parset_name) - structure_model.model = stru - structure_model.parameters = ParameterSet(name=parset_name) - structure_model.parameters.addParameterSet( - stru_diffpy_parset.getLattice() - ) - for atom_parset in stru_diffpy_parset.getScatterers(): - structure_model.parameters.addParameterSet(atom_parset) - return structure_model - - -def _create_parameter_set(names, values, parset_name): - """ - Construct a ParameterSet from a list of variables. - - Parameters - ---------- - names : list of str - The names of the variables. - values : list of values - The list of values corresponding to the variable names. - parset_name : str - The name of the ParameterSet. - - Returns - ------- - ParameterSet - The converted ParameterSet. - """ - parameter_set = ParameterSet(name=parset_name) - for name, value in zip(names, values): - parameter = Parameter(name) - parameter.setValue(value) - parameter_set.addParameter(parameter) - return parameter_set diff --git a/src/diffpy/apps/refinebase/refinement_session.py b/src/diffpy/apps/refinebase/refinement_session.py index d828c92..328677d 100644 --- a/src/diffpy/apps/refinebase/refinement_session.py +++ b/src/diffpy/apps/refinebase/refinement_session.py @@ -1,115 +1,40 @@ import uuid from collections import OrderedDict +import numpy +from diffpy.srfit.fitbase import FitRecipe from scipy.optimize import leastsq -from diffpy.apps.refinebase.refinable_model import ParameterSetTree -from diffpy.srfit.fitbase import FitContribution, FitRecipe, Profile -from diffpy.srfit.fitbase.parameterset import ParameterSet - class RefinementSession: - """ - A refinement session class that manages the refinement process. - - Attributes - ---------- - variables : list of Variable - The list of variables in the refinement session. - loss_functions : list of callable - The list of loss functions in the refinement session. - loss_function : callable - The loss function for the refinement session. - - Methods - ------- - register_loss_function() - Register the loss function for refinement. - register_master_loss_function() - Register the master loss function for refinement. - refine() - Perform the refinement. - """ - def __init__(self): - self.main_parameter_set = ParameterSet(name="main") - self.calculators = OrderedDict() - self.contributions = OrderedDict() self.recipes = OrderedDict() - def add_parameterSet(self, parset): - """Add a ParameterSet to the refinement session.""" - self.main_parameter_set.addParameterSet(parset) - - def add_parameter(self, parameter=None, name=None, delegates_to=None): - """Add a Parameter to the refinement session.""" - self.main_parameter_set.addParameterSet(parameter) - - def add_function(self, name, expression=None, ns={}): - pass - - def add_calculator(self, calculator): - pass - - def add_case( - self, - name=None, - profile=None, - x=None, - y=None, - dy=None, - expression=None, - xname=None, - ): - if profile is None: - profile = Profile() - profile.setObservedProfile(x, y, dy) - contribution = FitContribution(name=name) - contribution.setProfile(profile, xname=xname) - contribution.setEquation(expression) - self.main_parameter_set.addParameterSet(contribution) - self.contributions[name] = contribution - def solve( self, - case_names, - case_weights, - var_names, - initial_values, + profiles, + models, + variables, id=uuid.uuid4(), + weights=None, + initial_values=None, ): - # Initialize the recipe recipe = FitRecipe() self.recipes[id] = recipe - for name, weight in zip(case_names, case_weights): - recipe.addContribution(self.contributions[name], weight=weight) + if weights is None: + weights = numpy.ones(len(profiles)) / len(profiles) + for i in range(len(models)): + models[i].set_profile(profiles[i]) + recipe.addContribution(models[i]._contribution, weight=weights[i]) # Add variables - temp_parameter_tree = ParameterSetTree(self.main_parameter_set) - for name in var_names: - if name not in temp_parameter_tree.graph.nodes: - raise ValueError( - f"Variable '{name}' not found in the parameter tree. " - ) - recipe_var_names = [name.replace(".", "_") for name in var_names] - for i, name in enumerate(var_names): - recipe.addVar( - temp_parameter_tree.graph.nodes[name]["parameter"], - value=initial_values[i], - name=recipe_var_names[i], - ) + if initial_values is not None: + for var, val in zip(variables, initial_values): + var.value = val + for var in variables: + recipe.addVar(var) # Refine the recipe recipe.fix("all") - for name in recipe_var_names: - recipe.free(name) - leastsq(recipe.residual, recipe.values) - - def clear(self, level=None): - if level is None: - level = 1 - - if level <= 1: - self.main_parameter_set = ParameterSet(name="main") - if level <= 2: - self.contributions = OrderedDict() - if level <= 3: - self.recipe = OrderedDict() + recipe.residual() + for i in range(len(variables)): + recipe.free(variables[i].name) + leastsq(recipe.residual, recipe.getValues()) diff --git a/tests/conftest.py b/tests/conftest.py index e3b6313..d43dcdf 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,3 +17,17 @@ 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 ParametricModel + + model = ParametricModel("main") + submodel = ParametricModel("sub") + submodel.set_equation("a*x") + model.register_submodel("u", submodel) + model.set_equation("A*sin(u)") + submodel.prepare() + model.prepare() + return model, submodel diff --git a/tests/helper.py b/tests/helper.py index b4b1a4f..f557162 100644 --- a/tests/helper.py +++ b/tests/helper.py @@ -1,5 +1,4 @@ import numpy as np - from diffpy.srfit.fitbase import FitContribution, FitRecipe, Profile from diffpy.srfit.pdf import PDFGenerator, PDFParser from diffpy.srfit.structure import constrainAsSpaceGroup diff --git a/tests/test_parametric_model.py b/tests/test_parametric_model.py new file mode 100644 index 0000000..ee1335a --- /dev/null +++ b/tests/test_parametric_model.py @@ -0,0 +1,48 @@ +import numpy +import pytest + + +def test_parametric_model_graph(nested_sine_model): + model, submodel = nested_sine_model + # C1: Create a nested parametric model + # Expect the graph to be constructed correctly + expected_parameters = ["main.A", "main.sub.a", "main.sub.x"] + actual_parameters = list(model.parameters.keys()) + assert set(expected_parameters) == set(actual_parameters) + expected_nodes = ["main", "main.A", "main.sub", "main.sub.a", "main.sub.x"] + actual_nodes = list(model._graph.nodes) + assert set(expected_nodes).issubset(set(actual_nodes)) + 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) diff --git a/tests/test_refinable_model.py b/tests/test_refinable_model.py deleted file mode 100644 index 5c5ae48..0000000 --- a/tests/test_refinable_model.py +++ /dev/null @@ -1,89 +0,0 @@ -from diffpy.apps.refinebase.refinable_model import ( - ParameterParser, - ParameterSetTree, -) - - -def test_parse_pdf(): - # C1: reading a valid .gr file - # Expect return valid ParameterSet and meta data - profile_path = "tests/data/Ni.gr" - parser = ParameterParser() - profile_model, meta = parser._parse_pdf( - profile_path, parset_name="profile_parameterset" - ) - expected_parameter_names = ["x", "y", "dx", "dy"] - actual_parameter_names = [ - par.name for par in profile_model.parameters.iterPars() - ] - assert set(actual_parameter_names) == set(expected_parameter_names) - expected_meta = { - "stype": "X", - "qmin": 0.5, - "qmax": 25.0, - "filename": "tests/data/Ni.gr", - "bank": 0, - "nbanks": 1, - } - actual_meta = meta - assert actual_meta == expected_meta - - -def test_parse_structure(): - # C1: reading a valid .cif file - # Expect return valid ParameterSet - structure_path = "tests/data/Ni.cif" - parser = ParameterParser() - structure_model = parser._parse_structure(structure_path) - expected_parameter_names = ["a", "b", "c", "alpha", "beta", "gamma"] - actual_parameter_names = [ - name - for name in structure_model.parameters._parsets[ - "lattice" - ]._parameters.keys() - ] - assert set(actual_parameter_names) == set(expected_parameter_names) - expected_atom_parameter_names = [ - "x", - "y", - "z", - "Uiso", - "Biso", - "occ", - "occupancy", - ] - expected_atom_parameter_names += [ - f"U{j+1}{k+1}" for j in range(3) for k in range(3) - ] - expected_atom_parameter_names += [ - f"B{j+1}{k+1}" for j in range(3) for k in range(3) - ] - for i in range(len(structure_model.model)): - actual_atom_parameter_names = [ - name - for name in structure_model.parameters._parsets[ - f"Ni{i}" - ]._parameters.keys() - ] - assert set(actual_atom_parameter_names) == set( - expected_atom_parameter_names - ) - - -def test_parameter_tree(): - # C1: Check if the parameter tree is constructed correctly - # Expect the tree to have the corresponding nodes and keeps - # the same structure as the ParameterSet - structure_path = "tests/data/Ni.cif" - parser = ParameterParser() - structure_model = parser._parse_structure( - structure_path, parset_name="phase" - ) - parameters = structure_model.parameters - tree_view = ParameterSetTree(parameters) - expected_nodes = set( - ["phase.lattice", "phase.lattice.a", "phase.Ni0", "phase.Ni0.x"] - ) - assert expected_nodes.issubset(set(tree_view.graph.nodes)) - assert tree_view.graph.has_edge("phase.lattice", "phase.lattice.a") - assert tree_view.graph.has_edge("phase.Ni0", "phase.Ni0.x") diff --git a/tests/test_refinement_session.py b/tests/test_refinement_session.py index b315237..e73e652 100644 --- a/tests/test_refinement_session.py +++ b/tests/test_refinement_session.py @@ -1,52 +1,75 @@ import numpy +from diffpy.srfit.fitbase import ( + Profile, +) +from diffpy.srfit.pdf import PDFParser +from diffpy.structure import Structure -from diffpy.apps.refinebase.refinable_model import ParameterSetTree +from diffpy.apps.refinebase.parametric_model import ( + ParametricModel, + ParametricModelPDF, +) from diffpy.apps.refinebase.refinement_session import RefinementSession -def test_refinement_session(): +def test_refine_sine(): # C1: Refinement session without additional calculator or functions session = RefinementSession() xobs = numpy.linspace(-numpy.pi, numpy.pi, 100) - yobs = numpy.sin(xobs) + 1e-1 * numpy.random.normal(size=xobs.shape) - session.add_case( - name="sine_fit", - x=xobs, - y=yobs, - expression="A*sin(a*x)", - xname="x", - ) + yobs = numpy.sin(xobs) + 1e-2 * numpy.random.normal(size=xobs.shape) + sine_profile = Profile() + sine_profile.setObservedProfile(xobs, yobs) + sine_model = ParametricModel("sine_model") + sine_model.set_equation("A*sin(a*x)") + sine_model.prepare() session.solve( - case_names=["sine_fit"], - case_weights=[1.0], - var_names=["main.sine_fit.A", "main.sine_fit.a"], + profiles=[sine_profile], + models=[sine_model], + variables=[ + sine_model.parameters["sine_model.A"], + sine_model.parameters["sine_model.a"], + ], initial_values=[0.5, 0.5], ) - tree = ParameterSetTree(session.main_parameter_set).graph assert numpy.isclose( - tree.nodes["main.sine_fit.A"]["parameter"].value, 1.0, rtol=1e-2 + sine_model.parameters["sine_model.A"].value, + 1.0, + rtol=1e-2, ) assert numpy.isclose( - tree.nodes["main.sine_fit.a"]["parameter"].value, 1.0, rtol=1e-2 + sine_model.parameters["sine_model.a"].value, + 1.0, + rtol=1e-2, ) - # C2: Refinement session with one PDFCalculator - # parser = ParameterParser() - # profile_path = "tests/data/Ni.gr" - # structure_path = "tests/data/Ni.cif" - # profile_model, profile_meta = parser._parse_pdf(profile_path) - # structure_model = parser._parse_structure(structure_path) - - # session = RefinementSession() - # session.addParameterSet(profile_model.parameters) - # session.addParameterSet(structure_model.parameters) - # session.addParameter( - # ParameterAdapter( - # name="g1", - # callable=lambda: PDFCalculator(structure_model.model)[1] - # ) - # ) - # session.addParameter(ParameterAdapter(name="s1", value=1.0)) - # session.register_loss_function(name="l1", expression="s1*g1") - # session.set_master_loss_function(name="l1") - # session.refine() + +def test_refine_ni(): + # C1: Refinement session with one PDFCalculator + profile_path = "tests/data/Ni.gr" + profile = Profile() + parser = PDFParser() + parser.parseFile(profile_path) + profile.loadParsedData(parser) + profile.setCalculationRange(xmax=20) + stru = Structure() + structure_path = "tests/data/Ni.cif" + stru.read(structure_path) + + pdf_model = ParametricModelPDF("pdf", structure=stru, meta=profile.meta) + pdf_model.prepare() + ni_model = ParametricModel("ni_model") + ni_model.register_submodel("g", pdf_model) + ni_model.set_equation("s*g") + ni_model.prepare() + + ni_model.parameters["ni_model.s"].value = 1.0 + + session = RefinementSession() + session.solve( + profiles=[profile], + models=[ni_model], + variables=[ + ni_model.parameters["ni_model.s"], + pdf_model.parameters["pdf.phase.lattice.a"], + ], + ) From bf252edb82e98c126953bb359118e2f70cb1d4a4 Mon Sep 17 00:00:00 2001 From: Yuchen Xiao Date: Sat, 15 Aug 2026 21:00:57 -0400 Subject: [PATCH 8/8] feat: add MCP server --- requirements/pip.txt | 2 + .../apps/refinebase/refinement_server.py | 248 +++++++++++ .../apps/refinebase/refinement_session.py | 23 +- src/diffpy/apps/refinebase/util.py | 58 +++ tests/data/sine.dat | 400 ++++++++++++++++++ tests/test_refinement_server.py | 58 +++ 6 files changed, 788 insertions(+), 1 deletion(-) create mode 100644 src/diffpy/apps/refinebase/refinement_server.py create mode 100644 src/diffpy/apps/refinebase/util.py create mode 100644 tests/data/sine.dat create mode 100644 tests/test_refinement_server.py diff --git a/requirements/pip.txt b/requirements/pip.txt index 24ce15a..ab1429a 100644 --- a/requirements/pip.txt +++ b/requirements/pip.txt @@ -1 +1,3 @@ numpy +inline-snapshot +"mcp[cli]" diff --git a/src/diffpy/apps/refinebase/refinement_server.py b/src/diffpy/apps/refinebase/refinement_server.py new file mode 100644 index 0000000..70437f5 --- /dev/null +++ b/src/diffpy/apps/refinebase/refinement_server.py @@ -0,0 +1,248 @@ +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"] = uuid.uuid4(), +) -> str: + """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"] = uuid.uuid4(), +) -> 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, + ) + + 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"] = uuid.uuid4(), +) -> 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, + ) + + 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 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) + pdf_model.prepare() + 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 ParametricModel + + equation_model = ParametricModel(name=model_name) + equation_model.set_equation(equation) + equation_model.prepare() + 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.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.keys()] + + +@mcp.tool() +async def set_model_equation( + model_name: Annotated[str, "Name of the parametric model"], + equation: Annotated[str, "Equation to set for the parametric model"], +) -> str: + """Set or change the equation for a specific parametric model.""" + if model_name not in session.models: + raise ValueError(f"Model with ID {model_name} does not exist.") + + model = session.models[model_name] + model.set_equation(equation) + model.prepare() # Re-prepare the model after changing the equation + + return f"Equation for model {model_name} set successfully." + + +@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: + raise ValueError( + f"Parent model with ID {parent_model_name} does not exist." + ) + + if child_model_name not in session.models: + raise ValueError( + f"Child model with ID {child_model_name} does not exist." + ) + + parent_model = session.models[parent_model_name] + child_model = session.models[child_model_name] + + parent_model.register_submodel(symbol, child_model) + parent_model.prepare() + + 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. + """ + from diffpy.apps.refinebase.util import get_variable + + variable = get_variable(session.models, param_name) + variable.setValue(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: + raise ValueError(f"Model with ID {model_name} does not exist.") + + model = session.models[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: + raise ValueError(f"Model with ID {model_name} does not exist.") + + model = session.models[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 + from diffpy.apps.refinebase.util import get_variable + + profile_objs = [session.profiles[pid] for pid in profile_names] + model_objs = [session.models[mid] for mid in model_names] + variable_objs = [ + get_variable(session.models, 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 index 328677d..585201d 100644 --- a/src/diffpy/apps/refinebase/refinement_session.py +++ b/src/diffpy/apps/refinebase/refinement_session.py @@ -2,13 +2,34 @@ from collections import OrderedDict import numpy -from diffpy.srfit.fitbase import FitRecipe +from diffpy.srfit.fitbase import ( + FitRecipe, + Profile, +) from scipy.optimize import leastsq +from diffpy.apps.refinebase.parametric_model import ( + ParametricModel, +) + class RefinementSession: def __init__(self): self.recipes = OrderedDict() + self.profiles = OrderedDict() + self.models = OrderedDict() + + def add_profile(self, profile: Profile, profile_name: str = None): + if profile in self.profiles.values(): + raise ValueError("Profile already exists in the session.") + if profile_name is None: + profile_name = str(uuid.uuid4()) + self.profiles[profile_name] = profile + + def add_model(self, model: ParametricModel): + if model in self.models.values(): + raise ValueError("Model already exists in the session.") + self.models[model.name] = model def solve( self, diff --git a/src/diffpy/apps/refinebase/util.py b/src/diffpy/apps/refinebase/util.py new file mode 100644 index 0000000..2198a92 --- /dev/null +++ b/src/diffpy/apps/refinebase/util.py @@ -0,0 +1,58 @@ +def get_pdf_profile(profile_path: str): + from diffpy.srfit.fitbase import Profile + from diffpy.srfit.pdf import PDFParser + + profile = Profile() + parser = PDFParser() + parser.parseFile(profile_path) + profile.loadParsedData(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.structure import Structure + + from diffpy.apps.refinebase.parametric_model import ParametricModelPDF + + stru = Structure() + stru.read(structure_path) + pdf_model = ParametricModelPDF(name, structure=stru) + return pdf_model + + +def get_variable(models_dict, variable_name): + objs = variable_name.split(".") + if objs[0] not in models_dict: + raise ValueError(f"Model '{objs[0]}' not found in the session.") + if variable_name not in models_dict[objs[0]].parameters: + raise ValueError( + f"Variable '{variable_name}' not found in the model '{objs[0]}'." + ) + + return models_dict[objs[0]].parameters[variable_name] + + +# if __name__ == "__main__": +# import numpy as np +# xarray = np.linspace(-2*np.pi, 2*np.pi, 400) +# yarray = np.sin(xarray) + 0.05*np.random.normal(size=len(xarray)) +# X = np.stack((xarray,yarray), axis=1) +# np.savetxt("sine.dat",X) 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_refinement_server.py b/tests/test_refinement_server.py new file mode 100644 index 0000000..e803f7e --- /dev/null +++ b/tests/test_refinement_server.py @@ -0,0 +1,58 @@ +import numpy +import pytest +from mcp import Client + +from diffpy.apps.refinebase.refinement_server import mcp +from diffpy.apps.refinebase.util import get_variable + + +@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 + await mcp_client.call_tool( + "add_equation_model", + { + "model_name": "sine_model", + "equation": "A*sin(x)", + }, + ) + assert "sine_model" in session.models + await mcp_client.call_tool( + "set_model_param_value", + { + "param_name": "sine_model.A", + "value": 0.8, + }, + ) + variable_A = get_variable(session.models, "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)