Skip to content

Commit 76ca2b3

Browse files
committed
feat: use loadCrystal if the spacegroup might have multiple origins
1 parent 39b6d55 commit 76ca2b3

5 files changed

Lines changed: 290 additions & 60 deletions

File tree

‎src/diffpy/apps/refinebase/parametric_model.py‎

Lines changed: 29 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -199,27 +199,48 @@ def __init__(
199199
name,
200200
structure_file_path=None,
201201
from_model_name=None,
202-
structure_lib="Diffpy",
203202
):
204203
super().__init__(name=name)
205204
self.calc_obj = PDFGenerator(name)
205+
# NOTE: Certain space groups require dual origin handling.
206+
DUAL_ORIGIN_SG_NUMBERS = {
207+
48,
208+
50,
209+
59,
210+
68,
211+
70,
212+
85,
213+
86,
214+
88,
215+
125,
216+
126,
217+
129,
218+
130,
219+
133,
220+
134,
221+
137,
222+
138,
223+
141,
224+
142,
225+
201,
226+
203,
227+
222,
228+
224,
229+
227,
230+
228,
231+
}
206232
if structure_file_path is not None:
207233
stru_parser = get_parser("auto")
208234
structure = stru_parser.parse(
209235
Path(structure_file_path).read_text()
210236
)
211237
sg = getattr(stru_parser, "spacegroup", None)
212238
self.space_group_symbol = sg.short_name if sg is not None else "P1"
213-
if structure_lib == "Diffpy":
214-
self.calc_obj.setStructure(structure)
215-
elif structure_lib == "PyObjcryst":
239+
if sg.number in DUAL_ORIGIN_SG_NUMBERS:
216240
structure = loadCrystal(structure_file_path)
217241
self.calc_obj.setStructure(structure)
218242
else:
219-
raise ValueError(
220-
f"Unsupported structure library: {structure_lib} "
221-
"Please use 'Diffpy' or 'PyObjcryst'."
222-
)
243+
self.calc_obj.setStructure(structure)
223244
elif from_model_name is not None:
224245
self.calc_obj.setPhase(from_model_name.calc_obj.phase)
225246
self.space_group_symbol = from_model_name.space_group_symbol
Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
# diffpy_apps MCP server: how to use it
2+
3+
## Before starting
4+
5+
Call `list_models()` / `list_profiles()` first (session is a shared global singleton, not per-conversation). Call `clear()` to reset.
6+
7+
## MCP call-format rules
8+
9+
- **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.
10+
- **`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.
11+
- **`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.
12+
- **`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.
13+
- **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"`.
14+
15+
## Case A — nested equation models (no structure, e.g. fitting `A*sin(a*x)`)
16+
17+
```jsonc
18+
add_profile_from_arrays(xarray=[...], yarray=[...], profile_name="sine_profile")
19+
add_equation_model(model_name="sub", equation_str="a*x")
20+
add_equation_model(model_name="main", equation_str="A*sin(u)")
21+
combine_models(parent_model_name="main", child_model_names=["sub"], symbol="u") // "u" renames sub into the parent equation
22+
set_variables_value(name_value_dict={"main.A": 0.8, "main.sub.a": 0.5})
23+
solve(profile_names=["sine_profile"], model_names=["main"],
24+
variable_names=["main.A", "main.sub.a"], name="sine_fit")
25+
get_variable(variable_name="main.A")
26+
get_variable(variable_name="main.sub.a")
27+
```
28+
29+
## Case B — single PDF model (e.g. Ni), optionally wrapped for a free scale
30+
31+
```jsonc
32+
add_profile_from_file(profile_path="Ni.gr", profile_name="ni_profile")
33+
update_profile_meta(profile_name="ni_profile", meta={"qmin": 0.1})
34+
set_profile_calculation_range(profile_name="ni_profile", xmin=1.5, xmax=20, dx=0.01)
35+
36+
add_pdf_model(model_name="pdf", structure_file_path="Ni.cif")
37+
constrain_pdf_model_space_group_symmetry(model_name="pdf") // omit space_group to auto-detect
38+
set_variables_value(name_value_dict={"pdf.scale": 0.4, "pdf.delta2": 2, "pdf.qdamp": 0.04, "pdf.qbroad": 0.02})
39+
solve(profile_names=["ni_profile"], model_names=["pdf"],
40+
variable_names=["pdf.scale", "pdf.delta2", "pdf.qdamp", "pdf.qbroad"],
41+
include_sgpars=true, name="ni_fit")
42+
43+
// -- optional: promote to a free scale factor via a wrapping equation model --
44+
set_variables_value(name_value_dict={"pdf.scale": 1}) // freeze pdf's own scale at 1
45+
add_equation_model(model_name="ni_model", equation_str="s*pdf")
46+
combine_models(parent_model_name="ni_model", child_model_names=["pdf"], symbol="pdf")
47+
set_variables_value(name_value_dict={"ni_model.s": 0.4})
48+
solve(profile_names=["ni_profile"], model_names=["ni_model"],
49+
variable_names=["ni_model.s", "pdf.delta2", "pdf.qdamp", "pdf.qbroad"],
50+
include_sgpars=true, name="ni_fit_scaled")
51+
get_variable(variable_name="ni_model.pdf.phase.lattice.a") // combined child addressed as parent.child.param
52+
```
53+
54+
## Case C — multi-contribution joint refinement (Ni x-ray + Ni neutron + Si x-ray + mixed Si–Ni x-ray)
55+
56+
```jsonc
57+
add_profile_from_file(profile_path="ni-q27r60-xray.gr", profile_name="ni_xray")
58+
add_profile_from_file(profile_path="ni-q27r100-neutron.gr", profile_name="ni_neutron")
59+
add_profile_from_file(profile_path="si-q27r60-xray.gr", profile_name="si_xray")
60+
add_profile_from_file(profile_path="si90ni10-q27r60-xray.gr", profile_name="total_xray")
61+
set_profile_calculation_range(profile_name="ni_xray", xmax=20)
62+
set_profile_calculation_range(profile_name="ni_neutron", xmax=20)
63+
set_profile_calculation_range(profile_name="si_xray", xmax=20)
64+
set_profile_calculation_range(profile_name="total_xray", xmax=20)
65+
66+
add_pdf_model(model_name="pdf_ni", structure_file_path="Ni.cif")
67+
constrain_pdf_model_space_group_symmetry(model_name="pdf_ni") // constrain BEFORE cloning
68+
add_pdf_model(model_name="pdf_ni_neutron", from_model_name="pdf_ni")
69+
add_pdf_model(model_name="pdf_ni_partial", from_model_name="pdf_ni")
70+
71+
add_pdf_model(model_name="pdf_si", structure_file_path="Si.cif")
72+
constrain_pdf_model_space_group_symmetry(model_name="pdf_si")
73+
add_pdf_model(model_name="pdf_si_partial", from_model_name="pdf_si")
74+
75+
add_equation_model(model_name="main", equation_str="scale * (pdf_ni_partial + pdf_si_partial)")
76+
combine_models(parent_model_name="main", child_model_names=["pdf_ni_partial"], symbol="pdf_ni_partial")
77+
combine_models(parent_model_name="main", child_model_names=["pdf_si_partial"], symbol="pdf_si_partial")
78+
79+
set_variables_value(name_value_dict={
80+
"pdf_ni.qdamp": 0.055, "pdf_ni_neutron.qdamp": 0.030, "pdf_ni_partial.qdamp": 0.052,
81+
"pdf_si.qdamp": 0.051, "pdf_si_partial.qdamp": 0.052,
82+
"main.scale": 1.0, "pdf_si.scale": 1.0, "pdf_ni.scale": 1.0
83+
})
84+
85+
solve(
86+
profile_names=["ni_xray", "ni_neutron", "si_xray", "total_xray"],
87+
model_names=["pdf_ni", "pdf_ni_neutron", "pdf_si", "main"],
88+
residual_equations=["resv", "resv", "resv", "resv"],
89+
variable_names=[
90+
"pdf_ni.scale", "pdf_si.scale", "pdf_ni_neutron.scale", "main.scale", "pscale",
91+
"pdf_ni.phase.lattice.a", "pdf_ni.phase.Ni0.Uiso", "pdf_si.phase.a", "pdf_si.phase.Si.Biso",
92+
"ni_delta2", "si_delta2"
93+
],
94+
constraints=[
95+
{"pscale": 0.8, "ni_delta2": 2.5, "si_delta2": 2.5},
96+
{
97+
"pdf_ni.delta2": "ni_delta2", "pdf_ni_neutron.delta2": "ni_delta2", "main.pdf_ni_partial.delta2": "ni_delta2",
98+
"pdf_si.delta2": "si_delta2", "main.pdf_si_partial.delta2": "si_delta2",
99+
"main.pdf_si_partial.scale": "1 - pscale", "main.pdf_ni_partial.scale": "pscale"
100+
}
101+
],
102+
name="multi_fit"
103+
)
104+
105+
get_variable(variable_name="pdf_ni.phase.lattice.a")
106+
get_variable(variable_name="pdf_ni.phase.Ni0.Uiso") // Biso equivalent = value * 8 * pi^2
107+
get_variable(variable_name="pdf_si.phase.Si.Biso")
108+
```
109+
110+
Notes specific to this case: `pdf_ni`/`pdf_ni_neutron`/`pdf_ni_partial` share one structure (clones), so only `pdf_ni.phase.*` needs to be in `variable_names`. `delta2` is generator-level and NOT shared, hence the explicit constraints tying every clone's `delta2` back to one free `ni_delta2`/`si_delta2`. `pscale`/`1 - pscale` splits the mixed `total_xray` contribution's intensity between the Ni and Si partials.
111+
112+
## Unverified — check before relying on
113+
114+
- Exact pairing of `weights`/`restraints`/`metas` in `solve`.
115+
- 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.

0 commit comments

Comments
 (0)