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

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions news/misc_functionalities.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
**Added:**

* Add generator parallelism.
* Add soft bounds.
* add a tool to download cif file from database.

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
2 changes: 2 additions & 0 deletions requirements/conda.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,3 +8,5 @@ diffpy.srreal
diffpy.structure
networkx
mcp[cli]
mp-api
pymatgen
51 changes: 46 additions & 5 deletions src/diffpy/apps/refinebase/parametric_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
from pathlib import Path

import networkx as nx
import numpy
from pyobjcryst import loadCrystal

from diffpy.srfit.fitbase import FitContribution
Expand Down Expand Up @@ -245,7 +246,14 @@ def evaluate(self):
class ParametricModelPDF(ParametricModel):
# NOTE: qmin, qmax, stype(scattering type) are meta handled
# throughout the loaded profile in the refinement session
def __init__(self, name, structure, spacegroup_symbol="P1", finite=False):
def __init__(
self,
name,
structure,
spacegroup_symbol="P1",
finite=False,
run_parallel=True,
):
"""
Create a ParametricModelPDF instance from a structure object.

Expand All @@ -269,6 +277,25 @@ def __init__(self, name, structure, spacegroup_symbol="P1", finite=False):
self.space_group_symbol = spacegroup_symbol
self.sgpar_names = []
self._rebuild_graph()
if run_parallel:
try:
import multiprocessing
from multiprocessing import Pool

import psutil
except ImportError:
print(
"\nYou don't appear to have the "
"necessary packages for parallelization"
)
syst_cores = multiprocessing.cpu_count()
cpu_percent = psutil.cpu_percent()
avail_cores = numpy.floor(
(100 - cpu_percent) / (100.0 / syst_cores)
)
ncpu = int(numpy.max([1, avail_cores]))
pool = Pool(processes=ncpu)
self.calc_obj.parallel(ncpu=ncpu, mapfunc=pool.map)

def _hide_dependent_parameters(self, use_uiso=True):
if use_uiso:
Expand Down Expand Up @@ -545,7 +572,11 @@ def residual(self):


def create_pdf_model_from_file(
name, structure_file_path, library="Diffpy", finite=False
name,
structure_file_path,
library="Diffpy",
finite=False,
run_parallel=True,
):
"""Create a ParametricModelPDF by parsing a structure file."""
stru_parser = get_parser("auto")
Expand All @@ -557,16 +588,21 @@ def create_pdf_model_from_file(
) or library == "ObjCryst":
structure = loadCrystal(structure_file_path)
return ParametricModelPDF(
name, structure, spacegroup_symbol=spacegroup_symbol, finite=finite
name,
structure,
spacegroup_symbol=spacegroup_symbol,
finite=finite,
run_parallel=run_parallel,
)


def create_pdf_model_from_model(name, from_model):
def create_pdf_model_from_model(name, from_model, run_parallel=True):
"""Create a ParametricModelPDF sharing the phase of from_model."""
return ParametricModelPDF(
name,
from_model.calc_obj.phase,
spacegroup_symbol=from_model.space_group_symbol,
run_parallel=run_parallel,
)


Expand All @@ -577,6 +613,7 @@ def create_pdf_model_from_code(
global_namespace={},
local_structure_name="structure",
finite=False,
run_parallel=True,
):
"""Create a ParametricModelPDF by executing code that builds a structure.

Expand Down Expand Up @@ -618,5 +655,9 @@ def create_pdf_model_from_code(
)

return ParametricModelPDF(
name, structure, spacegroup_symbol=spacegroup_symbol, finite=finite
name,
structure,
spacegroup_symbol=spacegroup_symbol,
finite=finite,
run_parallel=run_parallel,
)
66 changes: 59 additions & 7 deletions src/diffpy/apps/refinebase/refinement_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from mcp.server.mcpserver.exceptions import ToolError

from diffpy.apps.refinebase.refinement_session import RefinementSession
from diffpy.apps.refinebase.util import download_mp_cifs

session = RefinementSession()
mcp = MCPServer("diffpy.apps")
Expand Down Expand Up @@ -56,6 +57,26 @@ async def refinement_playbook():
return f.read()


@mcp.tool()
@tool_errors
async def download_mp_cifs_tool(
query: Annotated[str, "Query for the Materials Project structures"],
start_index: Annotated[int, "Start index of the structures to download"],
end_index: Annotated[int, "End index of the structures to download"],
output_dir: Annotated[str, "Directory to save the CIF files"] = "mp_cifs",
api_key: Annotated[str | None, "Materials Project API key"] = None,
) -> list[str]:
"""
Download a consecutive range of Materials Project structures as CIF files.
"""
return [
str(path)
for path in download_mp_cifs(
query, start_index, end_index, output_dir, api_key
)
]


@mcp.tool()
@tool_errors
async def add_profile_from_file(
Expand All @@ -81,7 +102,6 @@ async def add_profile_from_file(
async def add_profile_from_arrays(
xarray: Annotated[list, "X-values of the profile"],
yarray: Annotated[list, "Y-values of the profile"],
dx: Annotated[list, "Uncertainties in the x-values"] = None,
dy: Annotated[list, "Uncertainties in the y-values"] = None,
profile_name: Annotated[str, "Unique name for the profile"] = None,
xname: Annotated[str, "Name of the x-axis for the profile"] = "x",
Expand All @@ -92,7 +112,6 @@ async def add_profile_from_arrays(
session.add_profile_from_arrays(
xarray,
yarray,
dx=dx,
dy=dy,
profile_name=profile_name,
xname=xname,
Expand Down Expand Up @@ -182,6 +201,21 @@ async def add_pdf_model(
from_model_name: Annotated[
str, "Name of the existing model to base the new model on"
] = None,
code: Annotated[
str, "Code that builds a structure to base the new model on"
] = None,
library: Annotated[
str, "Structure library to use ('Diffpy' or 'ObjCryst')"
] = "Diffpy",
global_namespace: Annotated[
dict, "Global namespace to execute 'code' in"
] = {},
local_structure_name: Annotated[
str, "Name of the structure variable assigned by 'code'"
] = "structure",
finite: Annotated[
bool, "Whether to use DebyePDFGenerator instead of PDFGenerator"
] = False,
) -> str:
"""
Add a structure-file-based parametric model to the refinement session.
Expand All @@ -193,18 +227,36 @@ async def add_pdf_model(
structure_file_path : str, optional
Path to the structure file.
from_model_name : str, optional
code : str, optional
Code that builds a structure to base the new model on.
library : str, optional
Structure library to use ('Diffpy' or 'ObjCryst').
global_namespace : dict, optional
Global namespace to execute 'code' in.
local_structure_name : str, optional
Name of the structure variable assigned by 'code'.
finite : bool, optional
Whether to use DebyePDFGenerator instead of PDFGenerator.

Notes
-----
'from_model_name' is used to create a new model referencing to an
existing computation object. For example, when the same phase's signal
is observed in multiple profiles, 'from_model_name' allows the refinement
backend to modify the same structure across multiple profiles.

Exactly one of 'structure_file_path', 'from_model_name', or 'code'
must be provided.
"""
session.add_pdf_model(
model_name=model_name,
structure_file_path=structure_file_path,
from_model_name=from_model_name,
code=code,
library=library,
global_namespace=global_namespace,
local_structure_name=local_structure_name,
finite=finite,
)
return f"Model {model_name} added successfully."

Expand Down Expand Up @@ -468,8 +520,8 @@ async def solve(
"and the second dict is variable-constraint_equation pair."
),
] = None,
restraints: Annotated[
list[str], "List of restraints to apply during the refinement"
bounds: Annotated[
dict, "Dictionary of bounds for variables or equations"
] = None,
name: Annotated[str, "Name of the refinement session"] = None,
weights: Annotated[
Expand Down Expand Up @@ -498,8 +550,8 @@ async def solve(
constraints : list[dict], optional
First dict is new_variable-initial value pair,
and the second dict is variable-constraint_equation pair.
restraints : list[str], optional
List of restraints to apply during the refinement.
bounds : dict, optional
Dictionary of bounds for variables or equations.
name : str, optional
Name of the refinement session.
weights : list[float], optional
Expand All @@ -526,7 +578,7 @@ async def solve(
variable_names,
residual_equations=residual_equations,
constraints=constraints,
restraints=restraints,
bounds=bounds,
name=name,
weights=weights,
metas=metas,
Expand Down
34 changes: 23 additions & 11 deletions src/diffpy/apps/refinebase/refinement_session.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,6 @@ def add_profile_from_arrays(
self,
xarray,
yarray,
dx=None,
dy=None,
profile_name: str = None,
xname: str = "x",
Expand All @@ -68,7 +67,7 @@ def add_profile_from_arrays(
if profile_name is None:
profile_name = str(uuid.uuid4())
profile = Profile()
profile.setObservedProfile(xarray, yarray, dx=dx, dy=dy)
profile.setObservedProfile(xobs=xarray, yobs=yarray, dyobs=dy)
profile.xpar.name = xname
profile._xname = xname
profile.ypar.name = yname
Expand Down Expand Up @@ -443,19 +442,19 @@ def _solve(
models,
variable_names,
constraints=None,
restraints=None,
bounds=None,
weights=None,
residual_equations=None,
metas=None,
verbose_iterations=0,
):
# NOTE: restraints to be implemented
recipe = FitRecipe()
self.recipes_dict[name] = recipe
if weights is None:
weights = numpy.ones(len(profiles))
if residual_equations is None:
residual_equations = ["chiv"] * len(profiles)
if bounds is None:
bounds = {}
if metas is not None:
for i in range(len(metas)):
profiles[i].meta.update(metas[i])
Expand Down Expand Up @@ -490,7 +489,20 @@ def _solve(
if var in recipe._parameters.values():
continue
recipe.add_variable(var, name=variable_names[i])

for eq_or_var_name, arg_dict in bounds.items():
lb = arg_dict.get("lower_bound", -numpy.inf)
ub = arg_dict.get("upper_bound", numpy.inf)
use_soft_bounds = arg_dict.get("use_soft_bounds", True)
if use_soft_bounds:
uncertainty = arg_dict.get("uncertainty", 1)
scaled = arg_dict.get("scaled", False)
eq_or_var_name = eq_or_var_name.replace(".", "_")
recipe.add_soft_bounds(
eq_or_var_name, lb, ub, sig=uncertainty, scaled=scaled
)
else:
par = self.get_variable(eq_or_var_name)["obj"]
par.bound_range(lb, ub)
recipe.free("all")
leastsq(recipe.residual, recipe.getValues())
# NOTE: non-scalar value will raise error in `get_results_string`
Expand All @@ -507,12 +519,11 @@ def solve(
variable_names=[],
residual_equations=None,
constraints=None,
restraints=None,
bounds=None,
name=uuid.uuid4(),
weights=None,
metas=None,
include_sgpars=False,
verbose_iterations=0,
):
profiles = []
for profile_name in profile_names:
Expand Down Expand Up @@ -551,19 +562,20 @@ def solve(
variable_names=variable_names,
residual_equations=residual_equations,
constraints=constraints,
restraints=restraints,
bounds=bounds,
name=name,
weights=weights,
metas=metas,
verbose_iterations=verbose_iterations,
)

def plot(self):
# NOTE: to be implemented
for id, recipe in self.recipes_dict.items():
recipe.plot_recipe()

def clear(self):
def clear(
self,
):
self.profiles_dict.clear()
self.models_dict.clear()
self.recipes_dict.clear()
Loading
Loading