Skip to content
Merged
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
23 changes: 23 additions & 0 deletions news/nanoparticle.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
**Added:**

* Added support for nanoparticle pdf refinement.

**Changed:**

* <news item>

**Deprecated:**

* <news item>

**Removed:**

* <news item>

**Fixed:**

* <news item>

**Security:**

* <news item>
134 changes: 97 additions & 37 deletions src/diffpy/apps/refinebase/parametric_model.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@
class ParametricModel:
def __init__(self, name):
self.name = name
self.calc_obj = FitContribution(name)
self._contribution = FitContribution(name)
self.calc_obj = self._contribution
self._graph = nx.DiGraph()
# all submodels will share the same profile
self._submodels = []
Expand Down Expand Up @@ -58,8 +59,7 @@ def _construct_parameter_graph(
)
self._graph.add_edge(parent_name, child_name)
self._construct_parameter_graph(
obj,
prefix=f"{parent_name}.",
obj, prefix=f"{parent_name}.", old_graph=old_graph
)

def register_submodel(self, submodel, symbol=None):
Expand All @@ -70,34 +70,47 @@ def register_submodel(self, submodel, symbol=None):
)
if symbol is None:
symbol = submodel.name
if symbol in self.calc_obj._parameters:
self.calc_obj._remove_parameter(self.calc_obj._parameters[symbol])
if symbol in self._contribution._parameters:
self._contribution._remove_parameter(
self._contribution._parameters[symbol]
)
if isinstance(submodel, ParametricModelPDF):
if symbol != submodel.name:
logger.warning(
f"ParametricModelPDF's name ({submodel.name}) does "
f"not match with the provided symbol ({symbol}) ",
)
self.calc_obj.add_profile_generator(submodel.calc_obj)
elif isinstance(submodel, ParametricModelEquation):
self.calc_obj._eqfactory.registerOperator(
symbol, submodel.calc_obj._eq
self._contribution.add_profile_generator(submodel.calc_obj)
elif isinstance(submodel, ParametricModelEquation) or isinstance(
submodel, ParametricModelFunction
):
self._contribution._eqfactory.registerOperator(
symbol, submodel._contribution._eq
)
self.calc_obj.add_parameter_set(submodel.calc_obj)
self._contribution.add_parameter_set(submodel._contribution)
else:
raise NotImplementedError(
"Only ParametricModelPDF and ParametricModelEquation "
"Only ParametricModelPDF, ParametricModelEquation, "
"and ParametricModelFunction "
"instances are supported to be registered as submodels."
)
if self.equation_str is not None:
self.calc_obj.set_equation(self.equation_str)
self._contribution.set_equation(self.equation_str)
submodel._rebuild_graph()
if f"{self.name}.{submodel.name}" not in self._graph.nodes:
self._graph.add_node(
symbol,
parameter=None,
constrained_or_constant=False,
)
self._graph.add_edge(self.name, symbol)
subgraph = submodel._graph.copy()
mapping = {node: f"{self.name}.{node}" for node in subgraph.nodes}
subgraph = nx.relabel_nodes(subgraph, mapping)
self._graph = nx.compose(self._graph, subgraph)
self._submodels.append(submodel)
self._rebuild_graph()

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

@property
def parameters(self):
return {
Expand Down Expand Up @@ -131,17 +144,17 @@ def independent_parameters(self):
}

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

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

def evaluate(self):
Expand All @@ -156,39 +169,77 @@ def residual(self):


class ParametricModelEquation(ParametricModel):
def __init__(self, name, equation_str=None, from_model_name=None):
def __init__(self, name, equation_str=None):
super().__init__(name=name)
self.equation_str = None
if from_model_name is not None:
for name, obj in from_model_name.calc_obj.__dict__.items():
if name not in ["name", "profile", "_observers"]:
setattr(self.calc_obj, name, obj)
if equation_str:
self.set_equation(equation_str)

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

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

def get_equation(self):
return self.equation_str

def evaluate(self):
yc = self.calc_obj._eq()
yc = self._contribution._eq()
if (
hasattr(self.calc_obj, "profile")
and self.calc_obj.profile is not None
hasattr(self._contribution, "profile")
and self._contribution.profile is not None
):
self.calc_obj.profile.ycalc = yc
self._contribution.profile.ycalc = yc
return yc

def residual(self):
return self.calc_obj.residual()
return self._contribution.residual()


class ParametricModelFunction(ParametricModel):
def __init__(self, name, function, argnames=None):
"""
Initialize a ParametricModelFunction instance.

function can be either a callable or a string representing
the pre-defined function.
One and only one of func or characteristic_func_name must be provided.
Allowed value for characteristic_func_name:
"spherical_particle",
"spheroidal_particle",
"lognormal_spherical_particle",
"sheet_particle",
"shell_particle",
"SASCF",
"sphericalCF",
"spheroidalCF",
"spheroidalCF2",
"lognormalSphericalCF",
"sheetCF",
"shellCF",
"shellCF2",
"""
super().__init__(name=name)
if isinstance(function, str):
import diffpy.srfit.pdf.characteristicfunctions

function = getattr(
diffpy.srfit.pdf.characteristicfunctions,
function,
)
self._contribution.register_function(function, argnames=argnames)
self._contribution.set_equation(function.__name__)
self.calc_obj = function
self._rebuild_graph()

def set_profile(self, profile, xname=None, yname=None, dyname=None):
self._contribution.set_profile(
profile, xname=xname, yname=yname, dyname=dyname
)
# no submodel is allowed ParametricModelFunction

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


class ParametricModelPDF(ParametricModel):
Expand All @@ -199,6 +250,7 @@ def __init__(
name,
structure_file_path=None,
from_model_name=None,
library="Diffpy",
):
super().__init__(name=name)
self.calc_obj = PDFGenerator(name)
Expand Down Expand Up @@ -236,7 +288,7 @@ def __init__(
)
sg = getattr(stru_parser, "spacegroup", None)
self.space_group_symbol = sg.short_name if sg is not None else "P1"
if sg.number in DUAL_ORIGIN_SG_NUMBERS:
if sg.number in DUAL_ORIGIN_SG_NUMBERS or library == "ObjCryst":
structure = loadCrystal(structure_file_path)
self.calc_obj.setStructure(structure)
else:
Expand Down Expand Up @@ -369,10 +421,18 @@ def constrain_symmetry(self, spacegroup_symbol=None, use_uiso=True):
"constrained_or_constant"
] = True

def _rebuild_graph(self):
old_graph = self._graph
self._graph = nx.DiGraph()
self._construct_parameter_graph(
# PDFGenerator itself holds parameters
self.calc_obj,
prefix="",
old_graph=old_graph,
)

def set_profile(self, profile):
self.calc_obj.set_profile(profile)
self._yname = self.calc_obj.profile.ypar.name
self._dyname = self.calc_obj.profile.dypar.name
# no submodel is allowed ParametricModelPDF

def evaluate(self):
Expand Down
18 changes: 9 additions & 9 deletions src/diffpy/apps/refinebase/refinement_playbook.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@ Call `list_models()` / `list_profiles()` first (session is a shared global singl

## MCP call-format rules

- **Always pass explicit `model_name`/`profile_name`.** `add_equation_model`/`add_pdf_model` default `model_name` to a UUID computed once at import time — every omitted call gets the *same* value. `profile_name` defaults to `None` and gets echoed straight into the confirmation message.
- **`combine_models(parent_model_name, child_model_names, symbol)`: call once per child.** `symbol` is one required string, no default. Use `symbol` = the child's own name, unless deliberately renaming an equation-model child (never a PDF child — mismatches there just log a warning, nothing renames). Only an equation model can be a parent.
- **`add_pdf_model(model_name=..., from_model_name=<existing>)` shares structure, not values.** Lattice/xyz/ADP(Uiso or Biso)/occupancy become the *same* live object as the source — set them once, on the source, never per clone. Generator params (`scale`, `qdamp`, `qbroad`, `delta1`, `delta2`) are NOT shared — set each clone's separately or tie them with a `solve` constraint. Constrain symmetry on the source **before** cloning.
- **Always pass explicit `model_name`/`profile_name`.** `add_equation_model`/`add_pdf_model` default `model_name` to a UUID computed once at import time — every omitted call gets the _same_ value. `profile_name` defaults to `None` and gets echoed straight into the confirmation message.
- **`combine_models(parent_model_name, child_model_names)`: call once per child.** The child is registered into the parent's equation under its own model name. Only an equation model can be a parent.
- **`add_pdf_model(model_name=..., from_model_name=<existing>)` shares structure, not values.** Lattice/xyz/ADP(Uiso or Biso)/occupancy become the _same_ live object as the source — set them once, on the source, never per clone. Generator params (`scale`, `qdamp`, `qbroad`, `delta1`, `delta2`) are NOT shared — set each clone's separately or tie them with a `solve` constraint. Constrain symmetry on the source **before** cloning.
- **`solve`**: `profile_names`/`model_names`/`residual_equations`/`weights` must be equal length, one entry per contribution. `constraints` is exactly two dicts: `[0]` helper-variable -> initial value, `[1]` variable -> constraint-equation string. Pass `name=` to inspect later via `list_recipe_parameters`. `include_sgpars=true` auto-adds symmetry-freed structural params instead of listing each one.
- **Parsing results**: `check_profile_meta`/`list_profiles`/`list_models` -> plain JSON. `get_variable`/`list_model_parameters`/`list_recipe_parameters` -> formatted text (`"Variable 'x': 0.42"`), parse it yourself. `get_model_evaluation`/`get_model_residual` write a JSON array to `data_path`; `get_profile_data` writes `{"xobs":[...], "yobs":[...]}` to `data_path` (no `dyobs`/calculated curve). `solve` returns a fit-report string. Errors come back as `"{ExceptionType}: message"`.

Expand All @@ -17,8 +17,8 @@ Call `list_models()` / `list_profiles()` first (session is a shared global singl
```jsonc
add_profile_from_arrays(xarray=[...], yarray=[...], profile_name="sine_profile")
add_equation_model(model_name="sub", equation_str="a*x")
add_equation_model(model_name="main", equation_str="A*sin(u)")
combine_models(parent_model_name="main", child_model_names=["sub"], symbol="u") // "u" renames sub into the parent equation
add_equation_model(model_name="main", equation_str="A*sin(sub)")
combine_models(parent_model_name="main", child_model_names=["sub"]) // "sub" is registered under its own model name
set_variables_value(name_value_dict={"main.A": 0.8, "main.sub.a": 0.5})
solve(profile_names=["sine_profile"], model_names=["main"],
variable_names=["main.A", "main.sub.a"], name="sine_fit")
Expand All @@ -43,7 +43,7 @@ solve(profile_names=["ni_profile"], model_names=["pdf"],
// -- optional: promote to a free scale factor via a wrapping equation model --
set_variables_value(name_value_dict={"pdf.scale": 1}) // freeze pdf's own scale at 1
add_equation_model(model_name="ni_model", equation_str="s*pdf")
combine_models(parent_model_name="ni_model", child_model_names=["pdf"], symbol="pdf")
combine_models(parent_model_name="ni_model", child_model_names=["pdf"])
set_variables_value(name_value_dict={"ni_model.s": 0.4})
solve(profile_names=["ni_profile"], model_names=["ni_model"],
variable_names=["ni_model.s", "pdf.delta2", "pdf.qdamp", "pdf.qbroad"],
Expand Down Expand Up @@ -73,8 +73,8 @@ constrain_pdf_model_space_group_symmetry(model_name="pdf_si")
add_pdf_model(model_name="pdf_si_partial", from_model_name="pdf_si")

add_equation_model(model_name="main", equation_str="scale * (pdf_ni_partial + pdf_si_partial)")
combine_models(parent_model_name="main", child_model_names=["pdf_ni_partial"], symbol="pdf_ni_partial")
combine_models(parent_model_name="main", child_model_names=["pdf_si_partial"], symbol="pdf_si_partial")
combine_models(parent_model_name="main", child_model_names=["pdf_ni_partial"])
combine_models(parent_model_name="main", child_model_names=["pdf_si_partial"])

set_variables_value(name_value_dict={
"pdf_ni.qdamp": 0.055, "pdf_ni_neutron.qdamp": 0.030, "pdf_ni_partial.qdamp": 0.052,
Expand Down Expand Up @@ -112,4 +112,4 @@ Notes specific to this case: `pdf_ni`/`pdf_ni_neutron`/`pdf_ni_partial` share on
## Unverified — check before relying on

- Exact pairing of `weights`/`restraints`/`metas` in `solve`.
- Whether the live connected `add_pdf_model` accepts `structure_lib` ("Diffpy"/"PyObjcryst") — seen on one live schema fetch but absent from the server source reviewed here.
- Whether the live connected `add_pdf_model` accepts `structure_lib` ("Diffpy"/"PyObjcryst") — seen on one live schema fetch but absent from the server source reviewed here.
Loading
Loading