xrdkit 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- xrdkit/__init__.py +226 -0
- xrdkit/broadening.py +1155 -0
- xrdkit/config.py +660 -0
- xrdkit/density.py +144 -0
- xrdkit/gsas2.py +915 -0
- xrdkit/gsas2_driver.py +2551 -0
- xrdkit/indexing.py +750 -0
- xrdkit/io.py +115 -0
- xrdkit/lattice.py +233 -0
- xrdkit/peaks.py +340 -0
- xrdkit/phases.py +494 -0
- xrdkit/plotting.py +1072 -0
- xrdkit/py.typed +0 -0
- xrdkit/sizestrain.py +539 -0
- xrdkit/structure.py +470 -0
- xrdkit-0.1.0.dist-info/METADATA +229 -0
- xrdkit-0.1.0.dist-info/RECORD +19 -0
- xrdkit-0.1.0.dist-info/WHEEL +4 -0
- xrdkit-0.1.0.dist-info/licenses/LICENSE +21 -0
xrdkit/gsas2_driver.py
ADDED
|
@@ -0,0 +1,2551 @@
|
|
|
1
|
+
"""Run one xrdkit job inside GSAS-II.
|
|
2
|
+
|
|
3
|
+
This script runs under GSAS-II's own Python, started by
|
|
4
|
+
:func:`xrdkit.gsas2.run_job` as ``python -B gsas2_driver.py job.json``. It
|
|
5
|
+
must not import xrdkit, which is not installed there. The job JSON names an
|
|
6
|
+
``action`` and its inputs, and ``gsas2_home``, the folder containing the
|
|
7
|
+
GSASII package; the result is written as JSON to ``job["result"]``.
|
|
8
|
+
|
|
9
|
+
Each action is a function of GSASIIscriptable and the job returning the result
|
|
10
|
+
dict, registered in ``ACTIONS``. GSASII is imported only once the job is read,
|
|
11
|
+
so that xrdkit can import this module to check stages without it.
|
|
12
|
+
|
|
13
|
+
Actions
|
|
14
|
+
-------
|
|
15
|
+
``create``
|
|
16
|
+
A new project with one histogram and its phases linked to it.
|
|
17
|
+
``refine``
|
|
18
|
+
A sequence of refinement stages on a project, created first if the job
|
|
19
|
+
carries the inputs for it, with the pattern, reflections and refined
|
|
20
|
+
instrument parameters exported after the last stage.
|
|
21
|
+
|
|
22
|
+
Stages
|
|
23
|
+
------
|
|
24
|
+
A stage is a dict with a ``name`` and the refinement flags to switch on:
|
|
25
|
+
|
|
26
|
+
``background``
|
|
27
|
+
``True`` for :data:`DEFAULT_BACKGROUND`, or ``{"type": ..., "terms": n}``.
|
|
28
|
+
``scale``, ``zero``, ``displacement``
|
|
29
|
+
Histogram scale, zero shift, and specimen displacement (``Shift`` in
|
|
30
|
+
Bragg-Brentano geometry, ``DisplaceX`` and ``DisplaceY`` in
|
|
31
|
+
Debye-Scherrer).
|
|
32
|
+
``instrument``
|
|
33
|
+
A list drawn from :data:`INSTRUMENT_KEYS`.
|
|
34
|
+
``cell``, ``le_bail``, ``phase_fractions``, ``size``, ``mustrain``, ``overall_uiso``
|
|
35
|
+
``True`` for every phase or a list of phase names. ``size`` and
|
|
36
|
+
``mustrain`` refine the isotropic crystallite size and microstrain,
|
|
37
|
+
making either isotropic if it was not. ``overall_uiso`` refines one
|
|
38
|
+
displacement parameter for every atom of the phase: when a stage first
|
|
39
|
+
switches it on, every atom is made isotropic with the Uiso the job's
|
|
40
|
+
``overall_uiso_start`` gives the phase (by name, or ``"*"``), and all
|
|
41
|
+
the Uiso are constrained equal.
|
|
42
|
+
``atoms``
|
|
43
|
+
A list of ``{"phase": name, "flags": "FXU"}``, any of F (occupancy),
|
|
44
|
+
X (position) and U (displacement), for every atom of the phase.
|
|
45
|
+
``atom_flags``
|
|
46
|
+
A list of ``{"phase": name, "labels": [...], "flags": "FXU"}``, for the
|
|
47
|
+
named atoms only. Atoms that share a site and have X refined are
|
|
48
|
+
constrained to move together, in the coordinates the site leaves free.
|
|
49
|
+
``uiso_groups``
|
|
50
|
+
``{phase: [[label, ...], ...]}``: groups of sites, each named by the
|
|
51
|
+
label of any atom on it, the Uiso of every atom on a group's sites
|
|
52
|
+
constrained to one value and refined, replacing the phase's groups or
|
|
53
|
+
overall Uiso before (which must be switched off in the same stage,
|
|
54
|
+
``"overall_uiso": false``). The atoms keep their Uiso as the start.
|
|
55
|
+
``coordinates``
|
|
56
|
+
``{phase: {label: "xz", ...}}``: the coordinates to refine on the site of
|
|
57
|
+
each named atom, any of x, y and z, or ``"all"`` for every one the site
|
|
58
|
+
leaves free. Every atom on the site is refined, the atoms that share it
|
|
59
|
+
constrained to move together; a free coordinate not named is held. A
|
|
60
|
+
coordinate the site ties to another by symmetry follows it, so naming
|
|
61
|
+
either frees both; one the site fixes cannot be named.
|
|
62
|
+
``origin``
|
|
63
|
+
``{phase: {"site": label, "axis": "z"}}``: the site whose coordinate
|
|
64
|
+
along a polar axis is held, whatever else is refined, to fix the origin
|
|
65
|
+
along it; ``{phase: None}`` drops it. A stage that would refine that
|
|
66
|
+
coordinate on every site of a phase polar along it fails, since the
|
|
67
|
+
origin would then float.
|
|
68
|
+
``occupancies``
|
|
69
|
+
A list of ``{"phase": name, "sites": [label, ...], "elements": [...]}``:
|
|
70
|
+
the occupancies of every atom of each named element on the named sites
|
|
71
|
+
refined, with the element's content over those sites, the sum of
|
|
72
|
+
multiplicity times occupancy, held at its value when the constraint is
|
|
73
|
+
set up. Each element must have an atom on every site named, if need be
|
|
74
|
+
added at occupancy 0 by an atom edit. With every element's content held,
|
|
75
|
+
so is the total over the sites, and with it the vacancies they hold
|
|
76
|
+
between them; each site's own total may change. Other atoms on the sites
|
|
77
|
+
keep their occupancies.
|
|
78
|
+
Flags carry over from stage to stage, a list growing by the new entries, a
|
|
79
|
+
``True`` or ``False`` replacing what was there, unless the stage has
|
|
80
|
+
``"reset": true``, when it starts again from nothing refined. The
|
|
81
|
+
constraints a stage needs replace those of the stage before whenever they
|
|
82
|
+
differ.
|
|
83
|
+
|
|
84
|
+
Structure
|
|
85
|
+
---------
|
|
86
|
+
A phase entry's ``atoms`` edits the structure read from its CIF within the
|
|
87
|
+
project, the CIF itself untouched: occupancies, positions and isotropic
|
|
88
|
+
displacement parameters changed, and atoms of other elements added on
|
|
89
|
+
existing sites with the positions and displacement parameters of the atoms
|
|
90
|
+
they share them with (:func:`check_atom_edits`). A refine job's
|
|
91
|
+
``background_start`` gives the background coefficients to start from, such
|
|
92
|
+
as those of an earlier refinement, and ``scale_start`` the histogram scale.
|
|
93
|
+
|
|
94
|
+
Sanity check
|
|
95
|
+
------------
|
|
96
|
+
After every stage each phase's atoms are checked (:func:`check_sanity`):
|
|
97
|
+
a negative Uiso or diagonal Uij, an occupancy outside 0 to 1 or a site whose
|
|
98
|
+
occupancies add up to more than 1, and a coordinate more than ``max_shift``
|
|
99
|
+
(fractional, :data:`DEFAULT_MAX_SHIFT` by default) from its position in the
|
|
100
|
+
reference, by default the atoms as the job found them before its first
|
|
101
|
+
stage. A job's ``sanity``, ``{"max_shift": ..., "reference": {phase:
|
|
102
|
+
[{"label": ..., "xyz": [...]}, ...]}}``, sets either. The flags are recorded
|
|
103
|
+
with each stage, and a new one rejects the stage (below).
|
|
104
|
+
|
|
105
|
+
Rejected, unsettled and failed stages
|
|
106
|
+
-------------------------------------
|
|
107
|
+
Each stage is recorded with a ``status``, one of :data:`STATUSES`.
|
|
108
|
+
|
|
109
|
+
``"clean"``
|
|
110
|
+
Settled, and raising no sanity flag the stage before it lacked. Kept.
|
|
111
|
+
``"unsettled"``
|
|
112
|
+
Reached ``max_passes`` without settling, and raised no new flag. It is
|
|
113
|
+
recorded with its ``largest_remaining_move``, the parameter that moved
|
|
114
|
+
most in its last pass and by how many esds, and is kept when the job's
|
|
115
|
+
``on_unsettled`` is ``"accept"``, the default; a job that would rather
|
|
116
|
+
not keep one sets ``on_unsettled`` to ``"reject"``.
|
|
117
|
+
``"flagged"``
|
|
118
|
+
Raised a sanity flag the stage before it did not have (for the first
|
|
119
|
+
stage, one the atoms did not have when the job found them), with the
|
|
120
|
+
job's ``on_flagged`` ``"accept"``. Kept.
|
|
121
|
+
``"rejected"``
|
|
122
|
+
Raised such a flag with ``on_flagged`` ``"reject"``, the default, or did
|
|
123
|
+
not settle with ``on_unsettled`` ``"reject"``. Rolled back.
|
|
124
|
+
``"failed"``
|
|
125
|
+
Raised. Rolled back, and the run ends there.
|
|
126
|
+
|
|
127
|
+
A rejected stage is recorded, result and all, with ``rejected_because``; the
|
|
128
|
+
project then goes back to the state the last stage kept left it in,
|
|
129
|
+
parameters, atoms and constraints, and is saved so, and the stage's own
|
|
130
|
+
entries are dropped from the stages, so that what it alone refined is held
|
|
131
|
+
in every stage after it. A failed stage is recorded with its ``error``.
|
|
132
|
+
|
|
133
|
+
Every run ends with a final model, whatever became of its stages: the values
|
|
134
|
+
and the exports are those of the last stage kept, and where no stage was
|
|
135
|
+
kept they are the model the job started from, computed without refining it.
|
|
136
|
+
The result's ``final_from`` names the stage they come from, or "job start".
|
|
137
|
+
|
|
138
|
+
Undetermined parameters
|
|
139
|
+
-----------------------
|
|
140
|
+
Each stage also records, as ``undetermined``, the refined atom parameters
|
|
141
|
+
the data leave undetermined (:func:`find_undetermined`): an occupancy whose
|
|
142
|
+
esd is more than half its allowed range, 0 to 1, and a coordinate or an
|
|
143
|
+
isotropic Uiso whose esd is larger than its shift from where the job found
|
|
144
|
+
it. The result's ``undetermined`` are those of the last stage kept.
|
|
145
|
+
|
|
146
|
+
Le Bail extraction
|
|
147
|
+
------------------
|
|
148
|
+
``G2Project.refine`` never resets the Le Bail intensities, so a phase whose
|
|
149
|
+
Le Bail flag is switched on in a new project would have no reflection list to
|
|
150
|
+
extract into. Whenever a stage switches it on for a phase, the driver first
|
|
151
|
+
does what the GSAS-II GUI does when asked to reset the structure factors:
|
|
152
|
+
every intensity is set afresh and ``le_bail_cycles`` Le Bail-only cycles are
|
|
153
|
+
run, refining nothing, before the least squares. The starting intensities
|
|
154
|
+
are random, and the generator is seeded so that a job gives the same result
|
|
155
|
+
each time it is run.
|
|
156
|
+
|
|
157
|
+
GSAS-II extracts the Le Bail intensities afresh within every least squares
|
|
158
|
+
cycle, so the function being minimised shifts under the refinement, which
|
|
159
|
+
then stops at the first cycle that raises chi squared, well short of the
|
|
160
|
+
minimum. A job's ``max_passes`` refines each stage again and again, up to
|
|
161
|
+
that many times, until no parameter moves by more than ``pass_tolerance``
|
|
162
|
+
esds from one pass to the next. The histogram scale is left out of that test
|
|
163
|
+
when every phase is extracted by Le Bail, since the extracted intensities
|
|
164
|
+
take up any change in it and it drifts freely. Each stage of a job that
|
|
165
|
+
gives ``max_passes``, one included, records its ``passes`` and whether it
|
|
166
|
+
settled; without one a stage is refined once and records neither.
|
|
167
|
+
|
|
168
|
+
Sample broadening
|
|
169
|
+
-----------------
|
|
170
|
+
GSAS-II gives every phase an isotropic crystallite size of 1 micron and a
|
|
171
|
+
microstrain of 1000, both Lorentzian, and adds the width they imply to the
|
|
172
|
+
instrumental width of every reflection. Refining instrument parameters on a
|
|
173
|
+
standard with those left in place makes the instrument terms absorb the
|
|
174
|
+
difference, and they then no longer describe the instrument. A refine job's
|
|
175
|
+
``broadening``, ``{phase name or "*": {"size": microns, "mustrain": value,
|
|
176
|
+
"lgmix": fraction}}``, any of the three, sets isotropic values, held fixed,
|
|
177
|
+
before the first stage.
|
|
178
|
+
|
|
179
|
+
GSAS-II holds a size to 0.001 to 10 microns, and at 10 microns a Lorentzian
|
|
180
|
+
size term still adds 0.088 / cos(theta) centidegrees, the same form as X,
|
|
181
|
+
which X then gives up. ``{"size": 10, "mustrain": 0, "lgmix": 0}`` makes the
|
|
182
|
+
size term Gaussian instead, adding some 0.0014 / cos^2(theta) centidegrees
|
|
183
|
+
squared to a variance of several, so that the sample adds next to nothing.
|
|
184
|
+
"""
|
|
185
|
+
|
|
186
|
+
import copy
|
|
187
|
+
import csv
|
|
188
|
+
import io
|
|
189
|
+
import json
|
|
190
|
+
import math
|
|
191
|
+
import random
|
|
192
|
+
import re
|
|
193
|
+
import sys
|
|
194
|
+
from pathlib import Path
|
|
195
|
+
|
|
196
|
+
# Only importers whose format name contains the hint are tried.
|
|
197
|
+
XRDML_HINT = "Panalytical"
|
|
198
|
+
XY_HINT = "comma/tab/semicolon"
|
|
199
|
+
CIF_HINT = "CIF"
|
|
200
|
+
|
|
201
|
+
DEFAULT_BACKGROUND = {"type": "chebyschev-1", "terms": 6}
|
|
202
|
+
|
|
203
|
+
# Profile parameters a stage may refine, besides the zero shift.
|
|
204
|
+
INSTRUMENT_KEYS = ("U", "V", "W", "X", "Y", "Z", "SH/L")
|
|
205
|
+
|
|
206
|
+
# Atom refinement flags: occupancy, position and displacement.
|
|
207
|
+
ATOM_FLAGS = "FXU"
|
|
208
|
+
|
|
209
|
+
STAGE_KEYS = frozenset(
|
|
210
|
+
{
|
|
211
|
+
"name",
|
|
212
|
+
"reset",
|
|
213
|
+
"background",
|
|
214
|
+
"scale",
|
|
215
|
+
"zero",
|
|
216
|
+
"displacement",
|
|
217
|
+
"cell",
|
|
218
|
+
"instrument",
|
|
219
|
+
"atoms",
|
|
220
|
+
"le_bail",
|
|
221
|
+
"phase_fractions",
|
|
222
|
+
"size",
|
|
223
|
+
"mustrain",
|
|
224
|
+
"overall_uiso",
|
|
225
|
+
"atom_flags",
|
|
226
|
+
"uiso_groups",
|
|
227
|
+
"coordinates",
|
|
228
|
+
"origin",
|
|
229
|
+
"occupancies",
|
|
230
|
+
}
|
|
231
|
+
)
|
|
232
|
+
BOOLEAN_FLAGS = ("scale", "zero", "displacement")
|
|
233
|
+
|
|
234
|
+
# Fractional coordinates, and the word that frees every one a site allows.
|
|
235
|
+
AXES = "xyz"
|
|
236
|
+
ALL_COORDINATES = "all"
|
|
237
|
+
|
|
238
|
+
# What the sanity check after each stage looks for, the fractional shift
|
|
239
|
+
# from the reference beyond which a coordinate is flagged, and how far an
|
|
240
|
+
# occupancy or a site's total may pass 0 or 1 by rounding alone.
|
|
241
|
+
SANITY_KINDS = ("negative_uiso", "occupancy", "coordinate_shift")
|
|
242
|
+
DEFAULT_MAX_SHIFT = 0.05
|
|
243
|
+
OCCUPANCY_TOLERANCE = 1.0e-6
|
|
244
|
+
# What becomes of a stage whose sanity check raises a new flag, and of one
|
|
245
|
+
# not settled after max_passes: kept, or rejected and the project taken back
|
|
246
|
+
# to the last stage kept.
|
|
247
|
+
POLICIES = ("accept", "reject")
|
|
248
|
+
DEFAULT_ON_FLAGGED = "reject"
|
|
249
|
+
DEFAULT_ON_UNSETTLED = "accept"
|
|
250
|
+
# A stage's status: refined and kept, clean, with a new flag let through,
|
|
251
|
+
# or not settled; refined and rejected; failed.
|
|
252
|
+
STATUSES = ("clean", "flagged", "unsettled", "rejected", "failed")
|
|
253
|
+
# The range an occupancy may take; a refined occupancy whose esd is more
|
|
254
|
+
# than half of it is undetermined.
|
|
255
|
+
OCCUPANCY_RANGE = (0.0, 1.0)
|
|
256
|
+
|
|
257
|
+
# Atoms closer than this in every fractional coordinate share a site.
|
|
258
|
+
SAME_SITE = 1.0e-4
|
|
259
|
+
PHASE_FLAGS = (
|
|
260
|
+
"cell",
|
|
261
|
+
"le_bail",
|
|
262
|
+
"phase_fractions",
|
|
263
|
+
"size",
|
|
264
|
+
"mustrain",
|
|
265
|
+
"overall_uiso",
|
|
266
|
+
)
|
|
267
|
+
|
|
268
|
+
# What a phase's atom edits may carry: the label of the atom to change or
|
|
269
|
+
# add, for a new atom its element and the label of the atom whose site and
|
|
270
|
+
# displacement parameters it shares, and the occupancy, position and
|
|
271
|
+
# isotropic displacement parameter to give it.
|
|
272
|
+
ATOM_EDIT_KEYS = frozenset({"label", "occupancy", "type", "copy", "xyz", "uiso"})
|
|
273
|
+
|
|
274
|
+
# Le Bail-only cycles run when a stage switches Le Bail extraction on, and
|
|
275
|
+
# the seed of the random starting intensities GSAS-II gives the reflections.
|
|
276
|
+
DEFAULT_LE_BAIL_CYCLES = 10
|
|
277
|
+
LE_BAIL_SEED = 0
|
|
278
|
+
|
|
279
|
+
# A stage refined in passes is settled when no parameter moves by more than
|
|
280
|
+
# this many esds from one pass to the next.
|
|
281
|
+
DEFAULT_PASS_TOLERANCE = 0.1
|
|
282
|
+
|
|
283
|
+
# In a phase flag, every phase.
|
|
284
|
+
ALL_PHASES = "*"
|
|
285
|
+
|
|
286
|
+
# Sample parameters the stages switch on and off, and the displacement
|
|
287
|
+
# parameters of each geometry.
|
|
288
|
+
SAMPLE_KEYS = ("Scale", "Shift", "DisplaceX", "DisplaceY")
|
|
289
|
+
DISPLACEMENT = {
|
|
290
|
+
"Bragg-Brentano": ["Shift"],
|
|
291
|
+
"Debye-Scherrer": ["DisplaceX", "DisplaceY"],
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
# Sample broadening a refine job may hold, and the isotropic crystallite
|
|
295
|
+
# size range in microns that GSAS-II clamps a size to when it saves one.
|
|
296
|
+
BROADENING_KEYS = frozenset({"size", "mustrain", "lgmix"})
|
|
297
|
+
MIN_SIZE = 0.001
|
|
298
|
+
MAX_SIZE = 10.0
|
|
299
|
+
|
|
300
|
+
# Instrument parameters reported after a refinement.
|
|
301
|
+
REPORTED_INSTRUMENT = ("Zero", *INSTRUMENT_KEYS)
|
|
302
|
+
|
|
303
|
+
# Lines of refinement output kept as the report of a failed stage.
|
|
304
|
+
FAILURE_LINES = 20
|
|
305
|
+
FAILURE_WORDS = re.compile(r"error|fail|singular|problem|abort", re.IGNORECASE)
|
|
306
|
+
# What GSAS-II prints when a refinement worked. A refinement of no cycles
|
|
307
|
+
# leaves no covariance to judge it by, so its output is read instead.
|
|
308
|
+
REFINED_MARK = "Refinement successful"
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
def _plain(value):
|
|
312
|
+
"""``value`` with numpy scalars and arrays turned into JSON types."""
|
|
313
|
+
if isinstance(value, dict):
|
|
314
|
+
return {str(key): _plain(item) for key, item in value.items()}
|
|
315
|
+
if isinstance(value, (list, tuple, set, frozenset)):
|
|
316
|
+
return [_plain(item) for item in value]
|
|
317
|
+
if hasattr(value, "tolist"):
|
|
318
|
+
return value.tolist()
|
|
319
|
+
if isinstance(value, float) and not math.isfinite(value):
|
|
320
|
+
return None
|
|
321
|
+
return value
|
|
322
|
+
|
|
323
|
+
|
|
324
|
+
# Stages
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def empty_flags():
|
|
328
|
+
"""Flags with nothing refined."""
|
|
329
|
+
return {
|
|
330
|
+
"background": None,
|
|
331
|
+
"scale": False,
|
|
332
|
+
"zero": False,
|
|
333
|
+
"displacement": False,
|
|
334
|
+
"instrument": [],
|
|
335
|
+
"cell": [],
|
|
336
|
+
"le_bail": [],
|
|
337
|
+
"phase_fractions": [],
|
|
338
|
+
"size": [],
|
|
339
|
+
"mustrain": [],
|
|
340
|
+
"overall_uiso": [],
|
|
341
|
+
"atoms": {},
|
|
342
|
+
"atom_flags": {},
|
|
343
|
+
"uiso_groups": {},
|
|
344
|
+
"coordinates": {},
|
|
345
|
+
"origin": {},
|
|
346
|
+
"occupancies": [],
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
def _background(value, name):
|
|
351
|
+
if value is False or value is None:
|
|
352
|
+
return None
|
|
353
|
+
if value is True:
|
|
354
|
+
return dict(DEFAULT_BACKGROUND)
|
|
355
|
+
if isinstance(value, dict):
|
|
356
|
+
unknown = set(value) - {"type", "terms"}
|
|
357
|
+
if unknown:
|
|
358
|
+
raise ValueError(
|
|
359
|
+
f"stage {name!r}: unknown background keys {sorted(unknown)}"
|
|
360
|
+
)
|
|
361
|
+
terms = value.get("terms", DEFAULT_BACKGROUND["terms"])
|
|
362
|
+
if not isinstance(terms, int) or isinstance(terms, bool) or terms < 1:
|
|
363
|
+
raise ValueError(f"stage {name!r}: background terms must be a positive int")
|
|
364
|
+
return {
|
|
365
|
+
"type": str(value.get("type", DEFAULT_BACKGROUND["type"])),
|
|
366
|
+
"terms": terms,
|
|
367
|
+
}
|
|
368
|
+
raise ValueError(f"stage {name!r}: background must be a bool or a dict")
|
|
369
|
+
|
|
370
|
+
|
|
371
|
+
def _phases(value, current, key, name):
|
|
372
|
+
"""A phase flag: every phase, some phases by name, or none."""
|
|
373
|
+
if value is True:
|
|
374
|
+
return [ALL_PHASES]
|
|
375
|
+
if value is False or value is None:
|
|
376
|
+
return []
|
|
377
|
+
if isinstance(value, str):
|
|
378
|
+
value = [value]
|
|
379
|
+
if not isinstance(value, list) or not all(isinstance(v, str) for v in value):
|
|
380
|
+
raise ValueError(
|
|
381
|
+
f"stage {name!r}: {key} must be a bool or a list of phase names"
|
|
382
|
+
)
|
|
383
|
+
if ALL_PHASES in current:
|
|
384
|
+
return current
|
|
385
|
+
return current + [phase for phase in value if phase not in current]
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def _instrument(value, current, name):
|
|
389
|
+
if isinstance(value, str):
|
|
390
|
+
value = [value]
|
|
391
|
+
if not isinstance(value, list):
|
|
392
|
+
raise TypeError(f"stage {name!r}: instrument must be a list")
|
|
393
|
+
unknown = [key for key in value if key not in INSTRUMENT_KEYS]
|
|
394
|
+
if unknown:
|
|
395
|
+
raise ValueError(
|
|
396
|
+
f"stage {name!r}: unknown instrument parameters {unknown}; "
|
|
397
|
+
f"choose from {', '.join(INSTRUMENT_KEYS)}"
|
|
398
|
+
)
|
|
399
|
+
chosen = set(current) | set(value)
|
|
400
|
+
return [key for key in INSTRUMENT_KEYS if key in chosen]
|
|
401
|
+
|
|
402
|
+
|
|
403
|
+
def _atoms(value, current, name):
|
|
404
|
+
if not isinstance(value, list):
|
|
405
|
+
raise TypeError(f"stage {name!r}: atoms must be a list")
|
|
406
|
+
atoms = dict(current)
|
|
407
|
+
for entry in value:
|
|
408
|
+
if not isinstance(entry, dict) or set(entry) != {"phase", "flags"}:
|
|
409
|
+
raise ValueError(
|
|
410
|
+
f'stage {name!r}: each atoms entry must be {{"phase": ..., "flags": ...}}'
|
|
411
|
+
)
|
|
412
|
+
flags = str(entry["flags"]).upper()
|
|
413
|
+
if any(flag not in ATOM_FLAGS for flag in flags):
|
|
414
|
+
raise ValueError(
|
|
415
|
+
f"stage {name!r}: atom flags {entry['flags']!r} must be drawn from "
|
|
416
|
+
f"{ATOM_FLAGS}"
|
|
417
|
+
)
|
|
418
|
+
chosen = set(atoms.get(entry["phase"], "")) | set(flags)
|
|
419
|
+
atoms[entry["phase"]] = "".join(flag for flag in ATOM_FLAGS if flag in chosen)
|
|
420
|
+
return atoms
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def _atom_flags(value, current, name):
|
|
424
|
+
"""Flags for named atoms: ``[{"phase", "labels", "flags"}]``, growing
|
|
425
|
+
the flags each atom already has."""
|
|
426
|
+
if not isinstance(value, list):
|
|
427
|
+
raise TypeError(f"stage {name!r}: atom_flags must be a list")
|
|
428
|
+
chosen = copy.deepcopy(current)
|
|
429
|
+
for entry in value:
|
|
430
|
+
if not isinstance(entry, dict) or set(entry) != {"phase", "labels", "flags"}:
|
|
431
|
+
raise ValueError(
|
|
432
|
+
f"stage {name!r}: each atom_flags entry must be "
|
|
433
|
+
'{"phase": ..., "labels": [...], "flags": ...}'
|
|
434
|
+
)
|
|
435
|
+
labels = entry["labels"]
|
|
436
|
+
if (
|
|
437
|
+
not isinstance(labels, list)
|
|
438
|
+
or not labels
|
|
439
|
+
or not all(isinstance(label, str) for label in labels)
|
|
440
|
+
):
|
|
441
|
+
raise ValueError(
|
|
442
|
+
f"stage {name!r}: atom_flags labels must be a list of labels"
|
|
443
|
+
)
|
|
444
|
+
flags = str(entry["flags"]).upper()
|
|
445
|
+
if not flags or any(flag not in ATOM_FLAGS for flag in flags):
|
|
446
|
+
raise ValueError(
|
|
447
|
+
f"stage {name!r}: atom flags {entry['flags']!r} must be drawn from "
|
|
448
|
+
f"{ATOM_FLAGS}"
|
|
449
|
+
)
|
|
450
|
+
phase = chosen.setdefault(str(entry["phase"]), {})
|
|
451
|
+
for label in labels:
|
|
452
|
+
joined = set(phase.get(label, "")) | set(flags)
|
|
453
|
+
phase[label] = "".join(flag for flag in ATOM_FLAGS if flag in joined)
|
|
454
|
+
return chosen
|
|
455
|
+
|
|
456
|
+
|
|
457
|
+
def _uiso_groups(value, current, name):
|
|
458
|
+
"""Uiso groups by phase, ``{phase: [[label, ...], ...]}``, each replacing
|
|
459
|
+
the phase's groups before; an empty list drops them."""
|
|
460
|
+
if not isinstance(value, dict):
|
|
461
|
+
raise TypeError(f"stage {name!r}: uiso_groups must map phases to groups")
|
|
462
|
+
groups = copy.deepcopy(current)
|
|
463
|
+
for phase, phase_groups in value.items():
|
|
464
|
+
if not isinstance(phase_groups, list) or not all(
|
|
465
|
+
isinstance(group, list)
|
|
466
|
+
and group
|
|
467
|
+
and all(isinstance(label, str) for label in group)
|
|
468
|
+
for group in phase_groups
|
|
469
|
+
):
|
|
470
|
+
raise ValueError(
|
|
471
|
+
f"stage {name!r}: the Uiso groups of {phase!r} must be lists of labels"
|
|
472
|
+
)
|
|
473
|
+
labels = [label for group in phase_groups for label in group]
|
|
474
|
+
if len(labels) != len(set(labels)):
|
|
475
|
+
raise ValueError(
|
|
476
|
+
f"stage {name!r}: an atom of {phase!r} is in more than one Uiso group"
|
|
477
|
+
)
|
|
478
|
+
if phase_groups:
|
|
479
|
+
groups[str(phase)] = [list(group) for group in phase_groups]
|
|
480
|
+
else:
|
|
481
|
+
groups.pop(str(phase), None)
|
|
482
|
+
return groups
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
def _axes(value, name, label):
|
|
486
|
+
"""Coordinates named for a site: letters of xyz in order, or "all"."""
|
|
487
|
+
text = value.lower() if isinstance(value, str) else ""
|
|
488
|
+
if text == ALL_COORDINATES:
|
|
489
|
+
return text
|
|
490
|
+
if not text or any(axis not in AXES for axis in text):
|
|
491
|
+
raise ValueError(
|
|
492
|
+
f"stage {name!r}: the coordinates of {label!r} must be drawn from "
|
|
493
|
+
f'xyz, or be "all"; got {value!r}'
|
|
494
|
+
)
|
|
495
|
+
return "".join(axis for axis in AXES if axis in text)
|
|
496
|
+
|
|
497
|
+
|
|
498
|
+
def _coordinates(value, current, name):
|
|
499
|
+
"""Coordinates to refine by site, ``{phase: {label: "xz" or "all"}}``,
|
|
500
|
+
each site's growing by the new ones."""
|
|
501
|
+
if not isinstance(value, dict):
|
|
502
|
+
raise TypeError(f"stage {name!r}: coordinates must map phases to sites")
|
|
503
|
+
chosen = copy.deepcopy(current)
|
|
504
|
+
for phase, sites in value.items():
|
|
505
|
+
if not isinstance(sites, dict) or not sites:
|
|
506
|
+
raise ValueError(
|
|
507
|
+
f"stage {name!r}: the coordinates of {phase!r} must map site "
|
|
508
|
+
"labels to coordinates"
|
|
509
|
+
)
|
|
510
|
+
phase_sites = chosen.setdefault(str(phase), {})
|
|
511
|
+
for label, axes in sites.items():
|
|
512
|
+
axes = _axes(axes, name, label)
|
|
513
|
+
before = phase_sites.get(str(label), "")
|
|
514
|
+
if ALL_COORDINATES in (axes, before):
|
|
515
|
+
phase_sites[str(label)] = ALL_COORDINATES
|
|
516
|
+
else:
|
|
517
|
+
phase_sites[str(label)] = "".join(
|
|
518
|
+
axis for axis in AXES if axis in axes or axis in before
|
|
519
|
+
)
|
|
520
|
+
return chosen
|
|
521
|
+
|
|
522
|
+
|
|
523
|
+
def _origin(value, current, name):
|
|
524
|
+
"""The site that holds the origin along a polar axis, by phase,
|
|
525
|
+
``{phase: {"site": label, "axis": "z"}}``, replacing any before; None
|
|
526
|
+
drops it."""
|
|
527
|
+
if not isinstance(value, dict):
|
|
528
|
+
raise TypeError(f"stage {name!r}: origin must map phases to a site")
|
|
529
|
+
origin = copy.deepcopy(current)
|
|
530
|
+
for phase, entry in value.items():
|
|
531
|
+
if entry is None:
|
|
532
|
+
origin.pop(str(phase), None)
|
|
533
|
+
continue
|
|
534
|
+
if (
|
|
535
|
+
not isinstance(entry, dict)
|
|
536
|
+
or set(entry) != {"site", "axis"}
|
|
537
|
+
or str(entry["axis"]).lower() not in tuple(AXES)
|
|
538
|
+
):
|
|
539
|
+
raise ValueError(
|
|
540
|
+
f"stage {name!r}: the origin of {phase!r} must be "
|
|
541
|
+
'{"site": label, "axis": "x", "y" or "z"}'
|
|
542
|
+
)
|
|
543
|
+
origin[str(phase)] = {
|
|
544
|
+
"site": str(entry["site"]),
|
|
545
|
+
"axis": str(entry["axis"]).lower(),
|
|
546
|
+
}
|
|
547
|
+
return origin
|
|
548
|
+
|
|
549
|
+
|
|
550
|
+
def _names(value):
|
|
551
|
+
"""Whether ``value`` is a non-empty list of distinct non-empty strings."""
|
|
552
|
+
return (
|
|
553
|
+
isinstance(value, list)
|
|
554
|
+
and bool(value)
|
|
555
|
+
and all(isinstance(item, str) and item for item in value)
|
|
556
|
+
and len(set(value)) == len(value)
|
|
557
|
+
)
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def _occupancies(value, current, name):
|
|
561
|
+
"""Occupancy constraints, ``[{"phase", "sites", "elements"}]``, growing
|
|
562
|
+
by the new ones."""
|
|
563
|
+
if not isinstance(value, list):
|
|
564
|
+
raise TypeError(f"stage {name!r}: occupancies must be a list")
|
|
565
|
+
chosen = copy.deepcopy(current)
|
|
566
|
+
for entry in value:
|
|
567
|
+
if not isinstance(entry, dict) or set(entry) != {"phase", "sites", "elements"}:
|
|
568
|
+
raise ValueError(
|
|
569
|
+
f"stage {name!r}: each occupancies entry must be "
|
|
570
|
+
'{"phase": ..., "sites": [...], "elements": [...]}'
|
|
571
|
+
)
|
|
572
|
+
if not _names(entry["sites"]) or len(entry["sites"]) < 2:
|
|
573
|
+
raise ValueError(
|
|
574
|
+
f"stage {name!r}: an occupancy constraint needs two or more "
|
|
575
|
+
f"distinct sites, got {entry['sites']!r}"
|
|
576
|
+
)
|
|
577
|
+
if not _names(entry["elements"]):
|
|
578
|
+
raise ValueError(
|
|
579
|
+
f"stage {name!r}: an occupancy constraint needs a list of "
|
|
580
|
+
f"distinct elements, got {entry['elements']!r}"
|
|
581
|
+
)
|
|
582
|
+
clean = {
|
|
583
|
+
"phase": str(entry["phase"]),
|
|
584
|
+
"sites": list(entry["sites"]),
|
|
585
|
+
"elements": list(entry["elements"]),
|
|
586
|
+
}
|
|
587
|
+
if clean not in chosen:
|
|
588
|
+
chosen.append(clean)
|
|
589
|
+
return chosen
|
|
590
|
+
|
|
591
|
+
|
|
592
|
+
def accumulate_stages(stages):
|
|
593
|
+
"""The flags in force at each stage, as a list of ``{"name", "flags"}``.
|
|
594
|
+
|
|
595
|
+
Raises
|
|
596
|
+
------
|
|
597
|
+
ValueError
|
|
598
|
+
If there are no stages, or a stage has an unknown key or a flag of the
|
|
599
|
+
wrong kind.
|
|
600
|
+
TypeError
|
|
601
|
+
If a stage is not a dict, or one of its lists or mappings is of the
|
|
602
|
+
wrong type.
|
|
603
|
+
"""
|
|
604
|
+
if not isinstance(stages, list) or not stages:
|
|
605
|
+
raise ValueError("stages must be a non-empty list")
|
|
606
|
+
flags = empty_flags()
|
|
607
|
+
accumulated = []
|
|
608
|
+
for index, stage in enumerate(stages, start=1):
|
|
609
|
+
if not isinstance(stage, dict):
|
|
610
|
+
raise TypeError(f"stage {index} is not a dict")
|
|
611
|
+
name = str(stage.get("name") or f"stage {index}")
|
|
612
|
+
unknown = set(stage) - STAGE_KEYS
|
|
613
|
+
if unknown:
|
|
614
|
+
raise ValueError(f"stage {name!r}: unknown keys {sorted(unknown)}")
|
|
615
|
+
flags = empty_flags() if stage.get("reset") else copy.deepcopy(flags)
|
|
616
|
+
if "background" in stage:
|
|
617
|
+
flags["background"] = _background(stage["background"], name)
|
|
618
|
+
for key in BOOLEAN_FLAGS:
|
|
619
|
+
if key in stage:
|
|
620
|
+
if not isinstance(stage[key], bool):
|
|
621
|
+
raise ValueError(f"stage {name!r}: {key} must be true or false")
|
|
622
|
+
flags[key] = stage[key]
|
|
623
|
+
if "instrument" in stage:
|
|
624
|
+
flags["instrument"] = _instrument(
|
|
625
|
+
stage["instrument"], flags["instrument"], name
|
|
626
|
+
)
|
|
627
|
+
for key in PHASE_FLAGS:
|
|
628
|
+
if key in stage:
|
|
629
|
+
flags[key] = _phases(stage[key], flags[key], key, name)
|
|
630
|
+
if "atoms" in stage:
|
|
631
|
+
flags["atoms"] = _atoms(stage["atoms"], flags["atoms"], name)
|
|
632
|
+
if "atom_flags" in stage:
|
|
633
|
+
flags["atom_flags"] = _atom_flags(
|
|
634
|
+
stage["atom_flags"], flags["atom_flags"], name
|
|
635
|
+
)
|
|
636
|
+
if "uiso_groups" in stage:
|
|
637
|
+
flags["uiso_groups"] = _uiso_groups(
|
|
638
|
+
stage["uiso_groups"], flags["uiso_groups"], name
|
|
639
|
+
)
|
|
640
|
+
if "coordinates" in stage:
|
|
641
|
+
flags["coordinates"] = _coordinates(
|
|
642
|
+
stage["coordinates"], flags["coordinates"], name
|
|
643
|
+
)
|
|
644
|
+
if "origin" in stage:
|
|
645
|
+
flags["origin"] = _origin(stage["origin"], flags["origin"], name)
|
|
646
|
+
if "occupancies" in stage:
|
|
647
|
+
flags["occupancies"] = _occupancies(
|
|
648
|
+
stage["occupancies"], flags["occupancies"], name
|
|
649
|
+
)
|
|
650
|
+
both = [
|
|
651
|
+
phase
|
|
652
|
+
for phase in flags["uiso_groups"]
|
|
653
|
+
if _selected(flags["overall_uiso"], phase)
|
|
654
|
+
]
|
|
655
|
+
if both:
|
|
656
|
+
raise ValueError(
|
|
657
|
+
f"stage {name!r}: {both} cannot have an overall Uiso and Uiso "
|
|
658
|
+
'groups at once; switch overall_uiso off ("overall_uiso": false)'
|
|
659
|
+
)
|
|
660
|
+
accumulated.append({"name": name, "flags": flags})
|
|
661
|
+
return accumulated
|
|
662
|
+
|
|
663
|
+
|
|
664
|
+
def check_broadening(broadening):
|
|
665
|
+
"""Check a ``broadening`` mapping, returning it with float values.
|
|
666
|
+
|
|
667
|
+
Raises
|
|
668
|
+
------
|
|
669
|
+
ValueError
|
|
670
|
+
If a phase is given anything but some of ``size``, ``mustrain`` and
|
|
671
|
+
``lgmix``, or a size lies outside the range GSAS-II holds it to, a
|
|
672
|
+
microstrain is negative or ``lgmix`` lies outside 0 to 1.
|
|
673
|
+
TypeError
|
|
674
|
+
If it is not a dict.
|
|
675
|
+
"""
|
|
676
|
+
if not isinstance(broadening, dict):
|
|
677
|
+
raise TypeError("broadening must map phase names to size and mustrain")
|
|
678
|
+
checked = {}
|
|
679
|
+
for phase, values in broadening.items():
|
|
680
|
+
if not isinstance(values, dict) or not values or set(values) - BROADENING_KEYS:
|
|
681
|
+
raise ValueError(
|
|
682
|
+
f"broadening of {phase!r} must give size, mustrain and/or lgmix, "
|
|
683
|
+
f"got {values!r}"
|
|
684
|
+
)
|
|
685
|
+
checked[phase] = {key: float(value) for key, value in values.items()}
|
|
686
|
+
size = checked[phase].get("size", MIN_SIZE)
|
|
687
|
+
if not MIN_SIZE <= size <= MAX_SIZE:
|
|
688
|
+
raise ValueError(
|
|
689
|
+
f"broadening of {phase!r}: size must lie between {MIN_SIZE} and "
|
|
690
|
+
f"{MAX_SIZE} microns, the range GSAS-II clamps it to, got {size}"
|
|
691
|
+
)
|
|
692
|
+
if checked[phase].get("mustrain", 0.0) < 0.0:
|
|
693
|
+
raise ValueError(f"broadening of {phase!r}: mustrain cannot be negative")
|
|
694
|
+
if not 0.0 <= checked[phase].get("lgmix", 0.0) <= 1.0:
|
|
695
|
+
raise ValueError(f"broadening of {phase!r}: lgmix must lie between 0 and 1")
|
|
696
|
+
return checked
|
|
697
|
+
|
|
698
|
+
|
|
699
|
+
def check_atom_edits(edits, phase):
|
|
700
|
+
"""Check a phase's atom edits, returning them with float occupancies.
|
|
701
|
+
|
|
702
|
+
Each edit is ``{"label": ..., "occupancy": ...}`` to change an atom's
|
|
703
|
+
site fraction, or ``{"label": new, "type": element, "copy": existing,
|
|
704
|
+
"occupancy": ...}`` to add an atom of another element on the site of an
|
|
705
|
+
existing one, with its displacement parameters. Either may also carry
|
|
706
|
+
``xyz``, three fractional coordinates to move the atom to, and ``uiso``,
|
|
707
|
+
to make it isotropic with that Uiso, as when a structure is set up from
|
|
708
|
+
an earlier refinement's.
|
|
709
|
+
|
|
710
|
+
Raises
|
|
711
|
+
------
|
|
712
|
+
ValueError
|
|
713
|
+
If an edit has unknown keys, no label, nothing to change, a new atom
|
|
714
|
+
without its element or occupancy, an element for an existing atom, an
|
|
715
|
+
occupancy outside 0 to 1, or an ``xyz`` or ``uiso`` that is not three
|
|
716
|
+
or one finite numbers.
|
|
717
|
+
TypeError
|
|
718
|
+
If the edits are not a list of dicts.
|
|
719
|
+
"""
|
|
720
|
+
if not isinstance(edits, list):
|
|
721
|
+
raise TypeError(f"atom edits of {phase!r} must be a list")
|
|
722
|
+
checked = []
|
|
723
|
+
for edit in edits:
|
|
724
|
+
if not isinstance(edit, dict):
|
|
725
|
+
raise TypeError(f"atom edit of {phase!r} is not a dict: {edit!r}")
|
|
726
|
+
unknown = set(edit) - ATOM_EDIT_KEYS
|
|
727
|
+
if unknown or "label" not in edit or set(edit) == {"label"}:
|
|
728
|
+
raise ValueError(
|
|
729
|
+
f"atom edit of {phase!r} needs a label and something to change "
|
|
730
|
+
"(occupancy, xyz, uiso), or a label, type, copy and occupancy; "
|
|
731
|
+
f"got {edit!r}"
|
|
732
|
+
)
|
|
733
|
+
if "copy" in edit and not {"type", "occupancy"} <= set(edit):
|
|
734
|
+
raise ValueError(
|
|
735
|
+
f"new atom {edit['label']!r} of {phase!r} needs a type and an occupancy"
|
|
736
|
+
)
|
|
737
|
+
if "type" in edit and "copy" not in edit:
|
|
738
|
+
raise ValueError(
|
|
739
|
+
f"atom {edit['label']!r} of {phase!r}: a type is given only for a "
|
|
740
|
+
"new atom, with the label of the atom to copy"
|
|
741
|
+
)
|
|
742
|
+
clean = {
|
|
743
|
+
key: str(value)
|
|
744
|
+
for key, value in edit.items()
|
|
745
|
+
if key in ("label", "type", "copy")
|
|
746
|
+
}
|
|
747
|
+
if "occupancy" in edit:
|
|
748
|
+
clean["occupancy"] = float(edit["occupancy"])
|
|
749
|
+
if not 0.0 <= clean["occupancy"] <= 1.0:
|
|
750
|
+
raise ValueError(
|
|
751
|
+
f"atom {clean['label']!r} of {phase!r}: occupancy must lie "
|
|
752
|
+
f"between 0 and 1, got {edit['occupancy']}"
|
|
753
|
+
)
|
|
754
|
+
if "xyz" in edit:
|
|
755
|
+
xyz = edit["xyz"]
|
|
756
|
+
if not isinstance(xyz, (list, tuple)) or len(xyz) != 3:
|
|
757
|
+
raise ValueError(
|
|
758
|
+
f"atom {clean['label']!r} of {phase!r}: xyz must be three "
|
|
759
|
+
f"fractional coordinates, got {xyz!r}"
|
|
760
|
+
)
|
|
761
|
+
clean["xyz"] = [float(value) for value in xyz]
|
|
762
|
+
if "uiso" in edit:
|
|
763
|
+
clean["uiso"] = float(edit["uiso"])
|
|
764
|
+
if not all(
|
|
765
|
+
math.isfinite(value)
|
|
766
|
+
for value in [*clean.get("xyz", []), clean.get("uiso", 0.0)]
|
|
767
|
+
):
|
|
768
|
+
raise ValueError(
|
|
769
|
+
f"atom {clean['label']!r} of {phase!r}: xyz and uiso must be finite"
|
|
770
|
+
)
|
|
771
|
+
checked.append(clean)
|
|
772
|
+
return checked
|
|
773
|
+
|
|
774
|
+
|
|
775
|
+
def edit_atoms(G2sc, phase, edits):
|
|
776
|
+
"""Change occupancies, positions and Uiso and add atoms on existing
|
|
777
|
+
sites, as :func:`check_atom_edits` describes, then let GSAS-II set the
|
|
778
|
+
phase up again for the elements it now holds.
|
|
779
|
+
|
|
780
|
+
Raises
|
|
781
|
+
------
|
|
782
|
+
ValueError
|
|
783
|
+
If an atom to change or copy is missing, a new label is taken, or a
|
|
784
|
+
new position would give an atom another site symmetry, which GSAS-II
|
|
785
|
+
does not work out again.
|
|
786
|
+
"""
|
|
787
|
+
edits = check_atom_edits(edits, phase.name)
|
|
788
|
+
cx, ct, cs, cia = phase.data["General"]["AtomPtrs"]
|
|
789
|
+
atoms = phase.data["Atoms"]
|
|
790
|
+
by_label = {atom[ct - 1]: atom for atom in atoms}
|
|
791
|
+
for edit in edits:
|
|
792
|
+
label = edit["label"]
|
|
793
|
+
if "copy" in edit:
|
|
794
|
+
if label in by_label:
|
|
795
|
+
raise ValueError(f"phase {phase.name!r} already has an atom {label!r}")
|
|
796
|
+
if edit["copy"] not in by_label:
|
|
797
|
+
raise ValueError(
|
|
798
|
+
f"phase {phase.name!r} has no atom {edit['copy']!r} to copy"
|
|
799
|
+
)
|
|
800
|
+
atom = copy.deepcopy(by_label[edit["copy"]])
|
|
801
|
+
atom[ct - 1] = label
|
|
802
|
+
atom[ct] = edit["type"]
|
|
803
|
+
atom[cia + 8] = random.randint(0, sys.maxsize)
|
|
804
|
+
atoms.append(atom)
|
|
805
|
+
by_label[label] = atom
|
|
806
|
+
elif label not in by_label:
|
|
807
|
+
raise ValueError(f"phase {phase.name!r} has no atom {label!r}")
|
|
808
|
+
atom = by_label[label]
|
|
809
|
+
if "occupancy" in edit:
|
|
810
|
+
atom[cx + 3] = edit["occupancy"]
|
|
811
|
+
if "xyz" in edit:
|
|
812
|
+
site = G2sc.G2spc.SytSym(edit["xyz"], phase.data["General"]["SGData"])[0]
|
|
813
|
+
if site.strip() != str(atom[cs]).strip():
|
|
814
|
+
raise ValueError(
|
|
815
|
+
f"atom {label!r} of {phase.name!r} at {edit['xyz']} would sit on "
|
|
816
|
+
f"a site of symmetry {site.strip()}, not {atom[cs]}"
|
|
817
|
+
)
|
|
818
|
+
atom[cx : cx + 3] = edit["xyz"]
|
|
819
|
+
if "uiso" in edit:
|
|
820
|
+
atom[cia] = "I"
|
|
821
|
+
atom[cia + 1] = edit["uiso"]
|
|
822
|
+
G2sc.SetupGeneral(phase.data, None)
|
|
823
|
+
|
|
824
|
+
|
|
825
|
+
def _xyz_esd(phase, index, site_symmetry, esds, xinel):
|
|
826
|
+
"""The esds of an atom's x, y and z: of the refined shifts dAx, dAy and
|
|
827
|
+
dAz, and for a coordinate its site ties to another, as GSAS-II's
|
|
828
|
+
``GetCSxinel`` codes give it, that other's esd times the multiplier; None
|
|
829
|
+
for one that is fixed or not refined."""
|
|
830
|
+
values = [esds.get(f"{phase.id}::dA{axis}:{index}") for axis in "xyz"]
|
|
831
|
+
if xinel is None:
|
|
832
|
+
return values
|
|
833
|
+
codes, multipliers = xinel(site_symmetry)[:2]
|
|
834
|
+
for axis in range(3):
|
|
835
|
+
if values[axis] is not None or not codes[axis]:
|
|
836
|
+
continue
|
|
837
|
+
for other in range(3):
|
|
838
|
+
if codes[other] == codes[axis] and values[other] is not None:
|
|
839
|
+
ratio = abs(multipliers[axis] / multipliers[other])
|
|
840
|
+
values[axis] = values[other] * ratio
|
|
841
|
+
break
|
|
842
|
+
return values
|
|
843
|
+
|
|
844
|
+
|
|
845
|
+
def _atom_table(phase, esds=None, xinel=None):
|
|
846
|
+
"""Every atom of the phase: label, element, position, occupancy, site
|
|
847
|
+
and displacement parameters, with the esds of refined coordinates and
|
|
848
|
+
Uiso. ``xinel`` is GSAS-II's ``GetCSxinel``, to give a coordinate its
|
|
849
|
+
site ties to another the same esd."""
|
|
850
|
+
cx, ct, cs, cia = phase.data["General"]["AtomPtrs"]
|
|
851
|
+
table = []
|
|
852
|
+
for index, atom in enumerate(phase.data["Atoms"]):
|
|
853
|
+
isotropic = atom[cia] == "I"
|
|
854
|
+
row = {
|
|
855
|
+
"label": atom[ct - 1],
|
|
856
|
+
"type": atom[ct],
|
|
857
|
+
"xyz": list(atom[cx : cx + 3]),
|
|
858
|
+
"occupancy": atom[cx + 3],
|
|
859
|
+
"site_symmetry": atom[cs],
|
|
860
|
+
"multiplicity": atom[cs + 1],
|
|
861
|
+
"adp": atom[cia],
|
|
862
|
+
"uiso": atom[cia + 1] if isotropic else None,
|
|
863
|
+
"uij": None if isotropic else list(atom[cia + 2 : cia + 8]),
|
|
864
|
+
}
|
|
865
|
+
if esds is not None:
|
|
866
|
+
row["uiso_esd"] = esds.get(f"{phase.id}::AUiso:{index}")
|
|
867
|
+
row["occupancy_esd"] = esds.get(f"{phase.id}::Afrac:{index}")
|
|
868
|
+
row["xyz_esd"] = _xyz_esd(phase, index, atom[cs], esds, xinel)
|
|
869
|
+
table.append(row)
|
|
870
|
+
return table
|
|
871
|
+
|
|
872
|
+
|
|
873
|
+
def _operators(phase):
|
|
874
|
+
"""Every symmetry operator of the phase's space group, centring and
|
|
875
|
+
inversion included, as ``{"rotation", "translation"}``."""
|
|
876
|
+
import numpy as np
|
|
877
|
+
|
|
878
|
+
sg = phase.data["General"].get("SGData")
|
|
879
|
+
if not sg:
|
|
880
|
+
return None
|
|
881
|
+
operators = []
|
|
882
|
+
for centring in sg.get("SGCen", [[0, 0, 0]]):
|
|
883
|
+
for rotation, translation in sg["SGOps"]:
|
|
884
|
+
for sign in (1, -1) if sg.get("SGInv") else (1,):
|
|
885
|
+
operators.append(
|
|
886
|
+
{
|
|
887
|
+
"rotation": (sign * np.asarray(rotation)).tolist(),
|
|
888
|
+
"translation": (
|
|
889
|
+
(sign * np.asarray(translation) + np.asarray(centring))
|
|
890
|
+
% 1.0
|
|
891
|
+
).tolist(),
|
|
892
|
+
}
|
|
893
|
+
)
|
|
894
|
+
return operators
|
|
895
|
+
|
|
896
|
+
|
|
897
|
+
def check_uiso_start(starts):
|
|
898
|
+
"""Check ``overall_uiso_start``: a positive Uiso by phase name or "*"."""
|
|
899
|
+
if not isinstance(starts, dict):
|
|
900
|
+
raise TypeError("overall_uiso_start must map phase names to a Uiso")
|
|
901
|
+
checked = {}
|
|
902
|
+
for phase, value in starts.items():
|
|
903
|
+
checked[str(phase)] = float(value)
|
|
904
|
+
if not checked[str(phase)] > 0.0:
|
|
905
|
+
raise ValueError(f"overall_uiso_start of {phase!r} must be positive")
|
|
906
|
+
return checked
|
|
907
|
+
|
|
908
|
+
|
|
909
|
+
def check_background_start(start):
|
|
910
|
+
"""Check ``background_start``, ``{"type": ..., "coefficients": [...]}``,
|
|
911
|
+
returning it with float coefficients, or None if not given.
|
|
912
|
+
|
|
913
|
+
Raises
|
|
914
|
+
------
|
|
915
|
+
ValueError
|
|
916
|
+
If it has other keys or no coefficients.
|
|
917
|
+
"""
|
|
918
|
+
if start is None:
|
|
919
|
+
return None
|
|
920
|
+
if not isinstance(start, dict) or set(start) - {"type", "coefficients"}:
|
|
921
|
+
raise ValueError(
|
|
922
|
+
f'background_start must be {{"type": ..., "coefficients": [...]}}, got {start!r}'
|
|
923
|
+
)
|
|
924
|
+
coefficients = [float(value) for value in start.get("coefficients") or []]
|
|
925
|
+
if not coefficients:
|
|
926
|
+
raise ValueError("background_start needs at least one coefficient")
|
|
927
|
+
return {
|
|
928
|
+
"type": str(start.get("type", DEFAULT_BACKGROUND["type"])),
|
|
929
|
+
"coefficients": coefficients,
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
|
|
933
|
+
def check_scale_start(value):
|
|
934
|
+
"""Check ``scale_start``, the histogram scale to start from: a positive
|
|
935
|
+
number, or None if not given."""
|
|
936
|
+
if value is None:
|
|
937
|
+
return None
|
|
938
|
+
value = float(value)
|
|
939
|
+
if not (math.isfinite(value) and value > 0.0):
|
|
940
|
+
raise ValueError(f"scale_start must be positive, got {value}")
|
|
941
|
+
return value
|
|
942
|
+
|
|
943
|
+
|
|
944
|
+
def check_sanity_settings(settings):
|
|
945
|
+
"""Check a job's ``sanity``, returning ``{"max_shift", "reference"}``
|
|
946
|
+
with the defaults filled in, the reference None if not given.
|
|
947
|
+
|
|
948
|
+
Raises
|
|
949
|
+
------
|
|
950
|
+
ValueError
|
|
951
|
+
If it has other keys, ``max_shift`` is not positive, or a phase's
|
|
952
|
+
reference is not a list of ``{"label": ..., "xyz": [x, y, z]}``.
|
|
953
|
+
TypeError
|
|
954
|
+
If it, or its reference, is not a dict.
|
|
955
|
+
"""
|
|
956
|
+
if settings is None:
|
|
957
|
+
settings = {}
|
|
958
|
+
if not isinstance(settings, dict):
|
|
959
|
+
raise TypeError("sanity must be a dict")
|
|
960
|
+
if set(settings) - {"max_shift", "reference"}:
|
|
961
|
+
raise ValueError(
|
|
962
|
+
f'sanity may give only "max_shift" and "reference", got {sorted(settings)}'
|
|
963
|
+
)
|
|
964
|
+
max_shift = float(settings.get("max_shift", DEFAULT_MAX_SHIFT))
|
|
965
|
+
if not max_shift > 0.0:
|
|
966
|
+
raise ValueError(f"sanity max_shift must be positive, got {max_shift}")
|
|
967
|
+
reference = settings.get("reference")
|
|
968
|
+
if reference is not None:
|
|
969
|
+
if not isinstance(reference, dict):
|
|
970
|
+
raise TypeError("the sanity reference must map phases to atoms")
|
|
971
|
+
checked = {}
|
|
972
|
+
for phase, atoms in reference.items():
|
|
973
|
+
if not isinstance(atoms, list) or not all(
|
|
974
|
+
isinstance(atom, dict)
|
|
975
|
+
and "label" in atom
|
|
976
|
+
and isinstance(atom.get("xyz"), (list, tuple))
|
|
977
|
+
and len(atom["xyz"]) == 3
|
|
978
|
+
for atom in atoms
|
|
979
|
+
):
|
|
980
|
+
raise ValueError(
|
|
981
|
+
f"the sanity reference of {phase!r} must be a list of "
|
|
982
|
+
'{"label": ..., "xyz": [x, y, z]}'
|
|
983
|
+
)
|
|
984
|
+
checked[str(phase)] = [
|
|
985
|
+
{"label": str(atom["label"]), "xyz": [float(v) for v in atom["xyz"]]}
|
|
986
|
+
for atom in atoms
|
|
987
|
+
]
|
|
988
|
+
reference = checked
|
|
989
|
+
return {"max_shift": max_shift, "reference": reference}
|
|
990
|
+
|
|
991
|
+
|
|
992
|
+
def set_overall_uiso(project, phase, start):
|
|
993
|
+
"""Make every atom of ``phase`` isotropic with Uiso ``start`` and
|
|
994
|
+
constrain all their Uiso to be one, returning the constraints added as
|
|
995
|
+
lists of variable names."""
|
|
996
|
+
cia = phase.data["General"]["AtomPtrs"][3]
|
|
997
|
+
atoms = phase.data["Atoms"]
|
|
998
|
+
for atom in atoms:
|
|
999
|
+
atom[cia] = "I"
|
|
1000
|
+
atom[cia + 1] = float(start)
|
|
1001
|
+
names = [f"{phase.id}::AUiso:{index}" for index in range(len(atoms))]
|
|
1002
|
+
if len(names) < 2:
|
|
1003
|
+
return []
|
|
1004
|
+
project.add_EquivConstr(names)
|
|
1005
|
+
return [names]
|
|
1006
|
+
|
|
1007
|
+
|
|
1008
|
+
def set_uiso_groups(project, phase, groups, start=None):
|
|
1009
|
+
"""Constrain the Uiso of every atom on each group's sites to be one,
|
|
1010
|
+
returning the constraints added as lists of variable names.
|
|
1011
|
+
|
|
1012
|
+
Each group lists sites by the label of any atom on them, and takes in
|
|
1013
|
+
every atom that shares one. The atoms keep their Uiso; one that is
|
|
1014
|
+
anisotropic is made isotropic with ``start``.
|
|
1015
|
+
|
|
1016
|
+
Raises
|
|
1017
|
+
------
|
|
1018
|
+
ValueError
|
|
1019
|
+
If a label is not an atom of the phase, an atom falls in two groups,
|
|
1020
|
+
or an anisotropic atom is grouped with no ``start`` given.
|
|
1021
|
+
"""
|
|
1022
|
+
_, ct, _, cia = phase.data["General"]["AtomPtrs"]
|
|
1023
|
+
atoms = phase.data["Atoms"]
|
|
1024
|
+
index = {atom[ct - 1]: position for position, atom in enumerate(atoms)}
|
|
1025
|
+
members = site_members(phase)
|
|
1026
|
+
added = []
|
|
1027
|
+
seen = set()
|
|
1028
|
+
for group in groups:
|
|
1029
|
+
missing = [label for label in group if label not in index]
|
|
1030
|
+
if missing:
|
|
1031
|
+
raise ValueError(f"phase {phase.name!r} has no atoms {missing}")
|
|
1032
|
+
group = list(dict.fromkeys(m for label in group for m in members[label]))
|
|
1033
|
+
twice = seen & set(group)
|
|
1034
|
+
if twice:
|
|
1035
|
+
raise ValueError(
|
|
1036
|
+
f"atoms {sorted(twice)} of {phase.name!r} are in more than one "
|
|
1037
|
+
"Uiso group"
|
|
1038
|
+
)
|
|
1039
|
+
seen |= set(group)
|
|
1040
|
+
for label in group:
|
|
1041
|
+
atom = atoms[index[label]]
|
|
1042
|
+
if atom[cia] != "I":
|
|
1043
|
+
if start is None:
|
|
1044
|
+
raise ValueError(
|
|
1045
|
+
f"atom {label!r} of {phase.name!r} is anisotropic; give "
|
|
1046
|
+
"overall_uiso_start for its group's Uiso"
|
|
1047
|
+
)
|
|
1048
|
+
atom[cia] = "I"
|
|
1049
|
+
atom[cia + 1] = float(start)
|
|
1050
|
+
if len(group) > 1:
|
|
1051
|
+
names = [f"{phase.id}::AUiso:{index[label]}" for label in group]
|
|
1052
|
+
project.add_EquivConstr(names)
|
|
1053
|
+
added.append(names)
|
|
1054
|
+
return added
|
|
1055
|
+
|
|
1056
|
+
|
|
1057
|
+
def _set_uiso_scheme(project, phase, flags, uiso_start, schemes):
|
|
1058
|
+
"""Put the Uiso constraints ``flags`` ask of ``phase`` in place, replacing
|
|
1059
|
+
those of another scheme set up before; a stage that asks for none leaves
|
|
1060
|
+
them as they are. ``schemes`` holds each phase's scheme and constraints."""
|
|
1061
|
+
if _selected(flags["overall_uiso"], phase.name):
|
|
1062
|
+
wanted = "overall"
|
|
1063
|
+
elif phase.name in flags["uiso_groups"]:
|
|
1064
|
+
wanted = tuple(tuple(group) for group in flags["uiso_groups"][phase.name])
|
|
1065
|
+
else:
|
|
1066
|
+
return
|
|
1067
|
+
current = schemes.get(phase.name)
|
|
1068
|
+
if current is not None and current[0] == wanted:
|
|
1069
|
+
return
|
|
1070
|
+
start = uiso_start.get(phase.name, uiso_start.get(ALL_PHASES))
|
|
1071
|
+
if current is not None:
|
|
1072
|
+
remove_constraints(project, current[1])
|
|
1073
|
+
if wanted == "overall":
|
|
1074
|
+
if start is None:
|
|
1075
|
+
raise ValueError(
|
|
1076
|
+
f"overall_uiso_start gives no Uiso for phase {phase.name!r}"
|
|
1077
|
+
)
|
|
1078
|
+
added = set_overall_uiso(project, phase, start)
|
|
1079
|
+
else:
|
|
1080
|
+
added = set_uiso_groups(project, phase, wanted, start)
|
|
1081
|
+
schemes[phase.name] = (wanted, added)
|
|
1082
|
+
|
|
1083
|
+
|
|
1084
|
+
def remove_constraints(project, added):
|
|
1085
|
+
"""Remove the phase constraints whose variables are those of each list
|
|
1086
|
+
of names in ``added``."""
|
|
1087
|
+
wanted = [set(names) for names in added]
|
|
1088
|
+
constraints = project.data["Constraints"]["data"]
|
|
1089
|
+
constraints["Phase"] = [
|
|
1090
|
+
constraint
|
|
1091
|
+
for constraint in constraints.get("Phase", [])
|
|
1092
|
+
if {str(term[1]) for term in constraint[:-3]} not in wanted
|
|
1093
|
+
]
|
|
1094
|
+
|
|
1095
|
+
|
|
1096
|
+
def atom_flag_strings(phase, flags):
|
|
1097
|
+
"""The refinement flags of each atom of ``phase``, by label: those given
|
|
1098
|
+
every atom; U for an overall Uiso or an atom on a site of a Uiso group;
|
|
1099
|
+
X for an atom on a site whose coordinates are refined; F for an atom in
|
|
1100
|
+
an occupancy constraint; and those given the atom by name.
|
|
1101
|
+
|
|
1102
|
+
Raises
|
|
1103
|
+
------
|
|
1104
|
+
ValueError
|
|
1105
|
+
If flags, a Uiso group or a site name an atom the phase does not
|
|
1106
|
+
have, or an occupancy constraint cannot be set up.
|
|
1107
|
+
"""
|
|
1108
|
+
name = phase.name
|
|
1109
|
+
ct = phase.data["General"]["AtomPtrs"][1]
|
|
1110
|
+
labels = [atom[ct - 1] for atom in phase.data["Atoms"]]
|
|
1111
|
+
members = site_members(phase)
|
|
1112
|
+
chosen = {label: set(flags["atoms"].get(name, "")) for label in labels}
|
|
1113
|
+
named = {
|
|
1114
|
+
label: set(value) for label, value in flags["atom_flags"].get(name, {}).items()
|
|
1115
|
+
}
|
|
1116
|
+
missing = set(named) - set(chosen)
|
|
1117
|
+
by_site = [(group, "U") for group in flags["uiso_groups"].get(name, [])]
|
|
1118
|
+
by_site.append((list(flags["coordinates"].get(name, {})), "X"))
|
|
1119
|
+
for group, flag in by_site:
|
|
1120
|
+
for label in group:
|
|
1121
|
+
if label not in members:
|
|
1122
|
+
missing.add(label)
|
|
1123
|
+
continue
|
|
1124
|
+
for member in members[label]:
|
|
1125
|
+
named.setdefault(member, set()).add(flag)
|
|
1126
|
+
if missing:
|
|
1127
|
+
raise ValueError(f"phase {name!r} has no atoms {sorted(missing)}")
|
|
1128
|
+
entries = [entry for entry in flags["occupancies"] if entry["phase"] == name]
|
|
1129
|
+
for constraint in occupancy_constraints(phase, entries):
|
|
1130
|
+
for label in constraint["atoms"]:
|
|
1131
|
+
named.setdefault(label, set()).add("F")
|
|
1132
|
+
for label in labels:
|
|
1133
|
+
if _selected(flags["overall_uiso"], name):
|
|
1134
|
+
chosen[label].add("U")
|
|
1135
|
+
chosen[label] |= named.get(label, set())
|
|
1136
|
+
return {
|
|
1137
|
+
label: "".join(flag for flag in ATOM_FLAGS if flag in value)
|
|
1138
|
+
for label, value in chosen.items()
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
|
|
1142
|
+
def _wrapped(value):
|
|
1143
|
+
"""A fractional difference brought into -1/2 to 1/2."""
|
|
1144
|
+
return value - round(value)
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def _cluster(positions):
|
|
1148
|
+
"""The indices of ``positions`` grouped by site: each within SAME_SITE
|
|
1149
|
+
of the first of its group in every fractional coordinate, whole cell
|
|
1150
|
+
translations aside, the groups in the order of their first members."""
|
|
1151
|
+
groups = []
|
|
1152
|
+
for index, xyz in enumerate(positions):
|
|
1153
|
+
for group in groups:
|
|
1154
|
+
first = positions[group[0]]
|
|
1155
|
+
if all(abs(_wrapped(a - b)) < SAME_SITE for a, b in zip(xyz, first)):
|
|
1156
|
+
group.append(index)
|
|
1157
|
+
break
|
|
1158
|
+
else:
|
|
1159
|
+
groups.append([index])
|
|
1160
|
+
return groups
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def _sites(phase):
|
|
1164
|
+
"""Every site of the phase, as a list of (index, label, site symmetry)
|
|
1165
|
+
of the atoms on it, sites in the order of their first atoms."""
|
|
1166
|
+
cx, ct, cs, _ = phase.data["General"]["AtomPtrs"]
|
|
1167
|
+
atoms = phase.data["Atoms"]
|
|
1168
|
+
groups = _cluster([atom[cx : cx + 3] for atom in atoms])
|
|
1169
|
+
return [[(i, atoms[i][ct - 1], atoms[i][cs]) for i in group] for group in groups]
|
|
1170
|
+
|
|
1171
|
+
|
|
1172
|
+
def site_members(phase):
|
|
1173
|
+
"""The labels of the atoms on each atom's site, by label."""
|
|
1174
|
+
members = {}
|
|
1175
|
+
for site in _sites(phase):
|
|
1176
|
+
labels = [label for _, label, _ in site]
|
|
1177
|
+
for label in labels:
|
|
1178
|
+
members[label] = labels
|
|
1179
|
+
return members
|
|
1180
|
+
|
|
1181
|
+
|
|
1182
|
+
def _element(atom_type):
|
|
1183
|
+
"""The element of a GSAS-II atom type, such as Sr of "Sr+2"."""
|
|
1184
|
+
match = re.match(r"[A-Z][a-z]?", str(atom_type))
|
|
1185
|
+
return match.group(0) if match else str(atom_type)
|
|
1186
|
+
|
|
1187
|
+
|
|
1188
|
+
def polar_axes(phase):
|
|
1189
|
+
"""The axes, as indices, along which the phase's space group lets the
|
|
1190
|
+
origin float: those that every symmetry operator leaves alone, so that
|
|
1191
|
+
moving every atom along one changes nothing; none for a phase without
|
|
1192
|
+
its symmetry operators."""
|
|
1193
|
+
operators = _operators(phase)
|
|
1194
|
+
if not operators:
|
|
1195
|
+
return []
|
|
1196
|
+
return [
|
|
1197
|
+
axis
|
|
1198
|
+
for axis in range(3)
|
|
1199
|
+
if all(
|
|
1200
|
+
[row[axis] for row in op["rotation"]] == [int(j == axis) for j in range(3)]
|
|
1201
|
+
for op in operators
|
|
1202
|
+
)
|
|
1203
|
+
]
|
|
1204
|
+
|
|
1205
|
+
|
|
1206
|
+
def coordinate_constraints(xinel, phase, flags, refined):
|
|
1207
|
+
"""The coordinate constraints ``flags`` ask of ``phase``.
|
|
1208
|
+
|
|
1209
|
+
``refined`` holds the labels of the atoms whose X flag is set, and
|
|
1210
|
+
``xinel`` is GSAS-II's ``GetCSxinel``, whose codes give the coordinates a
|
|
1211
|
+
site leaves free and those it ties together. On each site holding such
|
|
1212
|
+
atoms every coordinate the site leaves free is refined if one of them
|
|
1213
|
+
has X from ``atoms`` or ``atom_flags``, and otherwise those the site's
|
|
1214
|
+
``coordinates`` name; the origin's coordinate is never refined on its
|
|
1215
|
+
site. A coordinate the site ties to another counts as that other.
|
|
1216
|
+
|
|
1217
|
+
Returns the equivalences that make the atoms sharing a site move
|
|
1218
|
+
together in each refined coordinate, the holds on each free coordinate
|
|
1219
|
+
of their atoms that is not refined, both as lists of variable names, and
|
|
1220
|
+
the site's record by the label of its first atom: ``{"site": labels
|
|
1221
|
+
joined by "/", "refined": coordinates, "held": coordinates, "origin":
|
|
1222
|
+
whether it holds the origin}``, naming each coordinate once, as the
|
|
1223
|
+
first of those the site ties together.
|
|
1224
|
+
|
|
1225
|
+
Every atom on a site stays refined, since GSAS-II ignores an
|
|
1226
|
+
equivalence, and holds its parameters, unless all of them are varied.
|
|
1227
|
+
Where GSAS-II's own symmetry constraints make a parameter independent in
|
|
1228
|
+
one equivalence and dependent in another, it recasts them as general
|
|
1229
|
+
constraints, with a new variable of its own for the site's shift, and
|
|
1230
|
+
notes that it has done so.
|
|
1231
|
+
|
|
1232
|
+
Raises
|
|
1233
|
+
------
|
|
1234
|
+
ValueError
|
|
1235
|
+
If only some of the atoms on a site are refined, a label is not an
|
|
1236
|
+
atom of the phase, a site's coordinates or the origin name one the
|
|
1237
|
+
site fixes, a site's coordinates name the origin's, or every site of
|
|
1238
|
+
a phase polar along an axis would be refined along it.
|
|
1239
|
+
RuntimeError
|
|
1240
|
+
If ``xinel`` is None.
|
|
1241
|
+
"""
|
|
1242
|
+
if xinel is None:
|
|
1243
|
+
raise RuntimeError("coordinates need GSAS-II's GetCSxinel")
|
|
1244
|
+
name = phase.name
|
|
1245
|
+
general = set()
|
|
1246
|
+
whole = flags["atoms"].get(name, "")
|
|
1247
|
+
for label, value in flags["atom_flags"].get(name, {}).items():
|
|
1248
|
+
if "X" in value:
|
|
1249
|
+
general.add(label)
|
|
1250
|
+
listed = flags["coordinates"].get(name, {})
|
|
1251
|
+
origin = flags["origin"].get(name)
|
|
1252
|
+
sites = _sites(phase)
|
|
1253
|
+
site_of = {
|
|
1254
|
+
label: position for position, site in enumerate(sites) for _, label, _ in site
|
|
1255
|
+
}
|
|
1256
|
+
missing = [label for label in listed if label not in site_of]
|
|
1257
|
+
if origin is not None and origin["site"] not in site_of:
|
|
1258
|
+
missing.append(origin["site"])
|
|
1259
|
+
if missing:
|
|
1260
|
+
raise ValueError(f"phase {name!r} has no atoms {sorted(missing)}")
|
|
1261
|
+
requests = {}
|
|
1262
|
+
for label, axes in listed.items():
|
|
1263
|
+
request = requests.setdefault(site_of[label], {"all": False, "axes": ""})
|
|
1264
|
+
if axes == ALL_COORDINATES:
|
|
1265
|
+
request["all"] = True
|
|
1266
|
+
else:
|
|
1267
|
+
request["axes"] += axes
|
|
1268
|
+
|
|
1269
|
+
equivalences, holds, records, refined_axes = [], [], {}, {}
|
|
1270
|
+
for position, site in enumerate(sites):
|
|
1271
|
+
labels = [label for _, label, _ in site]
|
|
1272
|
+
chosen = [label for label in labels if label in refined]
|
|
1273
|
+
if not chosen:
|
|
1274
|
+
continue
|
|
1275
|
+
if len(chosen) != len(labels):
|
|
1276
|
+
raise ValueError(
|
|
1277
|
+
f"atoms {sorted(labels)} of {name!r} share a site; refine the "
|
|
1278
|
+
f"coordinates of all of them, not only {sorted(chosen)}"
|
|
1279
|
+
)
|
|
1280
|
+
codes = xinel(site[0][2])[0]
|
|
1281
|
+
independent = [
|
|
1282
|
+
axis for axis in range(3) if codes[axis] and codes[axis] not in codes[:axis]
|
|
1283
|
+
]
|
|
1284
|
+
|
|
1285
|
+
def first_of(axis, codes=codes, labels=labels, symmetry=site[0][2]):
|
|
1286
|
+
if not codes[axis]:
|
|
1287
|
+
raise ValueError(
|
|
1288
|
+
f"the site of {labels[0]!r} in {name!r} ({symmetry}) fixes "
|
|
1289
|
+
f"{AXES[axis]}; it cannot be refined or hold the origin"
|
|
1290
|
+
)
|
|
1291
|
+
return codes.index(codes[axis])
|
|
1292
|
+
|
|
1293
|
+
request = requests.get(position, {"all": False, "axes": ""})
|
|
1294
|
+
if "X" in whole or general & set(labels) or request["all"]:
|
|
1295
|
+
wanted = set(independent)
|
|
1296
|
+
else:
|
|
1297
|
+
wanted = set()
|
|
1298
|
+
explicit = {first_of(AXES.index(letter)) for letter in request["axes"]}
|
|
1299
|
+
wanted |= explicit
|
|
1300
|
+
is_origin = origin is not None and origin["site"] in labels
|
|
1301
|
+
if is_origin:
|
|
1302
|
+
held = first_of(AXES.index(origin["axis"]))
|
|
1303
|
+
if held in explicit:
|
|
1304
|
+
raise ValueError(
|
|
1305
|
+
f"{origin['axis']} of {origin['site']!r} in {name!r} holds the "
|
|
1306
|
+
"origin and cannot be refined"
|
|
1307
|
+
)
|
|
1308
|
+
wanted.discard(held)
|
|
1309
|
+
for axis in sorted(wanted):
|
|
1310
|
+
if len(site) > 1:
|
|
1311
|
+
equivalences.append(
|
|
1312
|
+
[f"{phase.id}::dA{AXES[axis]}:{index}" for index, _, _ in site]
|
|
1313
|
+
)
|
|
1314
|
+
for axis in independent:
|
|
1315
|
+
if axis not in wanted:
|
|
1316
|
+
holds += [
|
|
1317
|
+
[f"{phase.id}::dA{AXES[axis]}:{index}"] for index, _, _ in site
|
|
1318
|
+
]
|
|
1319
|
+
refined_axes[position] = {
|
|
1320
|
+
axis
|
|
1321
|
+
for axis in range(3)
|
|
1322
|
+
if codes[axis] and codes.index(codes[axis]) in wanted
|
|
1323
|
+
}
|
|
1324
|
+
records[labels[0]] = {
|
|
1325
|
+
"site": "/".join(labels),
|
|
1326
|
+
"refined": "".join(AXES[axis] for axis in sorted(wanted)),
|
|
1327
|
+
"held": "".join(AXES[axis] for axis in independent if axis not in wanted),
|
|
1328
|
+
"origin": is_origin,
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
for axis in polar_axes(phase):
|
|
1332
|
+
if all(
|
|
1333
|
+
position in refined_axes and axis in refined_axes[position]
|
|
1334
|
+
for position in range(len(sites))
|
|
1335
|
+
):
|
|
1336
|
+
raise ValueError(
|
|
1337
|
+
f"the space group of {name!r} is polar along {AXES[axis]}, and "
|
|
1338
|
+
f"refining {AXES[axis]} on every site leaves the origin free; "
|
|
1339
|
+
f"hold one site's {AXES[axis]} with a stage's origin"
|
|
1340
|
+
)
|
|
1341
|
+
return equivalences, holds, records
|
|
1342
|
+
|
|
1343
|
+
|
|
1344
|
+
def occupancy_constraints(phase, entries):
|
|
1345
|
+
"""The constraint equations the occupancy constraints ``entries`` of
|
|
1346
|
+
``phase`` stand for, one for each element of each, as ``{"element",
|
|
1347
|
+
"sites", "atoms", "names", "multipliers", "total"}``: the atoms of the
|
|
1348
|
+
element on the named sites, their occupancy variables and
|
|
1349
|
+
multiplicities, and the element's content over the sites now, the sum
|
|
1350
|
+
of multiplicity times occupancy, which the constraint holds.
|
|
1351
|
+
|
|
1352
|
+
Raises
|
|
1353
|
+
------
|
|
1354
|
+
ValueError
|
|
1355
|
+
If a site is not an atom's label, two labels are on one site, an
|
|
1356
|
+
element has no atom on a site named, or an atom falls in two
|
|
1357
|
+
constraints.
|
|
1358
|
+
"""
|
|
1359
|
+
cx, ct, cs, _ = phase.data["General"]["AtomPtrs"]
|
|
1360
|
+
atoms = phase.data["Atoms"]
|
|
1361
|
+
index = {atom[ct - 1]: i for i, atom in enumerate(atoms)}
|
|
1362
|
+
sites = _sites(phase)
|
|
1363
|
+
site_of = {i: position for position, site in enumerate(sites) for i, _, _ in site}
|
|
1364
|
+
constraints, used = [], set()
|
|
1365
|
+
for entry in entries:
|
|
1366
|
+
missing = [label for label in entry["sites"] if label not in index]
|
|
1367
|
+
if missing:
|
|
1368
|
+
raise ValueError(f"phase {phase.name!r} has no atoms {missing}")
|
|
1369
|
+
positions = [site_of[index[label]] for label in entry["sites"]]
|
|
1370
|
+
if len(set(positions)) != len(positions):
|
|
1371
|
+
raise ValueError(
|
|
1372
|
+
f"the occupancy sites {entry['sites']} of {phase.name!r} name "
|
|
1373
|
+
"one site twice"
|
|
1374
|
+
)
|
|
1375
|
+
for element in entry["elements"]:
|
|
1376
|
+
chosen = []
|
|
1377
|
+
for label, position in zip(entry["sites"], positions):
|
|
1378
|
+
on_site = [
|
|
1379
|
+
i
|
|
1380
|
+
for i, _, _ in sites[position]
|
|
1381
|
+
if _element(atoms[i][ct]) == element
|
|
1382
|
+
]
|
|
1383
|
+
if not on_site:
|
|
1384
|
+
raise ValueError(
|
|
1385
|
+
f"phase {phase.name!r} has no {element} on the site of "
|
|
1386
|
+
f"{label!r}; add one with an atom edit, at occupancy 0"
|
|
1387
|
+
)
|
|
1388
|
+
chosen += on_site
|
|
1389
|
+
taken = used & set(chosen)
|
|
1390
|
+
if taken:
|
|
1391
|
+
raise ValueError(
|
|
1392
|
+
f"atoms {sorted(atoms[i][ct - 1] for i in taken)} of "
|
|
1393
|
+
f"{phase.name!r} are in two occupancy constraints"
|
|
1394
|
+
)
|
|
1395
|
+
used |= set(chosen)
|
|
1396
|
+
multipliers = [float(atoms[i][cs + 1]) for i in chosen]
|
|
1397
|
+
constraints.append(
|
|
1398
|
+
{
|
|
1399
|
+
"element": element,
|
|
1400
|
+
"sites": list(entry["sites"]),
|
|
1401
|
+
"atoms": [atoms[i][ct - 1] for i in chosen],
|
|
1402
|
+
"names": [f"{phase.id}::Afrac:{i}" for i in chosen],
|
|
1403
|
+
"multipliers": multipliers,
|
|
1404
|
+
"total": sum(
|
|
1405
|
+
m * float(atoms[i][cx + 3]) for m, i in zip(multipliers, chosen)
|
|
1406
|
+
),
|
|
1407
|
+
}
|
|
1408
|
+
)
|
|
1409
|
+
return constraints
|
|
1410
|
+
|
|
1411
|
+
|
|
1412
|
+
def _replace_constraints(project, managed, key, wanted, add, note):
|
|
1413
|
+
"""Put in place the constraints ``wanted`` stands for, under ``key`` in
|
|
1414
|
+
``managed``, replacing those set up there before unless they are the
|
|
1415
|
+
same, and return the note kept with them. ``add`` adds them, returning
|
|
1416
|
+
the lists of names of the constraints it added; ``note`` is kept with
|
|
1417
|
+
them when they are new."""
|
|
1418
|
+
current = managed.get(key)
|
|
1419
|
+
if current is not None and current[0] == wanted:
|
|
1420
|
+
return current[2]
|
|
1421
|
+
if current is not None:
|
|
1422
|
+
remove_constraints(project, current[1])
|
|
1423
|
+
managed[key] = (wanted, add() if wanted else [], note)
|
|
1424
|
+
return note
|
|
1425
|
+
|
|
1426
|
+
|
|
1427
|
+
def set_structure_constraints(project, phase, flags, xinel, managed):
|
|
1428
|
+
"""Put the coordinate and occupancy constraints ``flags`` ask of
|
|
1429
|
+
``phase`` in place, replacing those of the stage before where they
|
|
1430
|
+
differ, and return what they do: ``{"coordinates": by site,
|
|
1431
|
+
"occupancy_constraints": [...]}``, each only if there are any. See
|
|
1432
|
+
:func:`coordinate_constraints` and :func:`occupancy_constraints`."""
|
|
1433
|
+
notes = {}
|
|
1434
|
+
refined = {
|
|
1435
|
+
label
|
|
1436
|
+
for label, value in atom_flag_strings(phase, flags).items()
|
|
1437
|
+
if "X" in value
|
|
1438
|
+
}
|
|
1439
|
+
equivalences, holds, records = (
|
|
1440
|
+
coordinate_constraints(xinel, phase, flags, refined)
|
|
1441
|
+
if refined
|
|
1442
|
+
else ([], [], {})
|
|
1443
|
+
)
|
|
1444
|
+
|
|
1445
|
+
def add_coordinates():
|
|
1446
|
+
for names in equivalences:
|
|
1447
|
+
project.add_EquivConstr(names)
|
|
1448
|
+
for names in holds:
|
|
1449
|
+
project.add_HoldConstr(names)
|
|
1450
|
+
return [*equivalences, *holds]
|
|
1451
|
+
|
|
1452
|
+
_replace_constraints(
|
|
1453
|
+
project,
|
|
1454
|
+
managed,
|
|
1455
|
+
(phase.name, "coordinates"),
|
|
1456
|
+
(tuple(map(tuple, equivalences)), tuple(map(tuple, holds))),
|
|
1457
|
+
add_coordinates,
|
|
1458
|
+
None,
|
|
1459
|
+
)
|
|
1460
|
+
if records:
|
|
1461
|
+
notes["coordinates"] = records
|
|
1462
|
+
|
|
1463
|
+
entries = [entry for entry in flags["occupancies"] if entry["phase"] == phase.name]
|
|
1464
|
+
constraints = occupancy_constraints(phase, entries)
|
|
1465
|
+
|
|
1466
|
+
def add_occupancies():
|
|
1467
|
+
for constraint in constraints:
|
|
1468
|
+
project.add_EqnConstr(
|
|
1469
|
+
constraint["total"], constraint["names"], constraint["multipliers"]
|
|
1470
|
+
)
|
|
1471
|
+
return [constraint["names"] for constraint in constraints]
|
|
1472
|
+
|
|
1473
|
+
# The totals are left out of what identifies the constraints: once set
|
|
1474
|
+
# up, they hold them, and the occupancies they are worked out from move.
|
|
1475
|
+
note = _replace_constraints(
|
|
1476
|
+
project,
|
|
1477
|
+
managed,
|
|
1478
|
+
(phase.name, "occupancies"),
|
|
1479
|
+
tuple(
|
|
1480
|
+
(c["element"], tuple(c["names"]), tuple(c["multipliers"]))
|
|
1481
|
+
for c in constraints
|
|
1482
|
+
),
|
|
1483
|
+
add_occupancies,
|
|
1484
|
+
[{k: v for k, v in c.items() if k != "names"} for c in constraints],
|
|
1485
|
+
)
|
|
1486
|
+
if note:
|
|
1487
|
+
notes["occupancy_constraints"] = note
|
|
1488
|
+
return notes
|
|
1489
|
+
|
|
1490
|
+
|
|
1491
|
+
def check_sanity(atoms, reference=None, max_shift=DEFAULT_MAX_SHIFT):
|
|
1492
|
+
"""What looks wrong with a phase's atoms, given as the driver's atom
|
|
1493
|
+
table gives them, as a list of ``{"kind", "atoms", "value",
|
|
1494
|
+
"message"}``, ``kind`` one of :data:`SANITY_KINDS`.
|
|
1495
|
+
|
|
1496
|
+
Flagged are a negative Uiso, or diagonal Uij of an anisotropic atom; an
|
|
1497
|
+
occupancy outside 0 to 1, and a site whose atoms' occupancies add up to
|
|
1498
|
+
more than 1, beyond OCCUPANCY_TOLERANCE; and, with ``reference``, a list
|
|
1499
|
+
of ``{"label", "xyz"}``, a site whose coordinate has moved more than
|
|
1500
|
+
``max_shift`` (fractional, whole cells aside) from the position of its
|
|
1501
|
+
first atom there.
|
|
1502
|
+
"""
|
|
1503
|
+
flags = []
|
|
1504
|
+
|
|
1505
|
+
def flag(kind, labels, value, message):
|
|
1506
|
+
flags.append(
|
|
1507
|
+
{"kind": kind, "atoms": labels, "value": value, "message": message}
|
|
1508
|
+
)
|
|
1509
|
+
|
|
1510
|
+
for atom in atoms:
|
|
1511
|
+
label = atom["label"]
|
|
1512
|
+
if atom.get("adp", "I") == "I":
|
|
1513
|
+
if atom.get("uiso") is not None and atom["uiso"] < 0.0:
|
|
1514
|
+
flag(
|
|
1515
|
+
"negative_uiso",
|
|
1516
|
+
[label],
|
|
1517
|
+
atom["uiso"],
|
|
1518
|
+
f"{label} Uiso {atom['uiso']:.4f}",
|
|
1519
|
+
)
|
|
1520
|
+
else:
|
|
1521
|
+
for key, value in zip(("U11", "U22", "U33"), atom["uij"][:3]):
|
|
1522
|
+
if value < 0.0:
|
|
1523
|
+
flag("negative_uiso", [label], value, f"{label} {key} {value:.4f}")
|
|
1524
|
+
occupancy = atom["occupancy"]
|
|
1525
|
+
if not -OCCUPANCY_TOLERANCE <= occupancy <= 1.0 + OCCUPANCY_TOLERANCE:
|
|
1526
|
+
flag("occupancy", [label], occupancy, f"{label} occupancy {occupancy:.4f}")
|
|
1527
|
+
start = {entry["label"]: entry["xyz"] for entry in reference or []}
|
|
1528
|
+
for group in _cluster([atom["xyz"] for atom in atoms]):
|
|
1529
|
+
labels = [atoms[i]["label"] for i in group]
|
|
1530
|
+
site = "/".join(labels)
|
|
1531
|
+
if len(group) > 1:
|
|
1532
|
+
total = sum(atoms[i]["occupancy"] for i in group)
|
|
1533
|
+
if total > 1.0 + OCCUPANCY_TOLERANCE:
|
|
1534
|
+
flag("occupancy", labels, total, f"site {site} occupancy {total:.4f}")
|
|
1535
|
+
first = next((i for i in group if atoms[i]["label"] in start), None)
|
|
1536
|
+
if first is None:
|
|
1537
|
+
continue
|
|
1538
|
+
then = start[atoms[first]["label"]]
|
|
1539
|
+
for axis, (now, before) in enumerate(zip(atoms[first]["xyz"], then)):
|
|
1540
|
+
shift = _wrapped(now - before)
|
|
1541
|
+
if abs(shift) > max_shift:
|
|
1542
|
+
flag(
|
|
1543
|
+
"coordinate_shift",
|
|
1544
|
+
labels,
|
|
1545
|
+
shift,
|
|
1546
|
+
f"{site} {AXES[axis]} moved {shift:+.4f}",
|
|
1547
|
+
)
|
|
1548
|
+
return flags
|
|
1549
|
+
|
|
1550
|
+
|
|
1551
|
+
def find_undetermined(atoms, start=None):
|
|
1552
|
+
"""The refined atom parameters of a phase that the data leave
|
|
1553
|
+
undetermined, as a list of ``{"atom", "parameter", "value", "esd",
|
|
1554
|
+
"message"}``, ``parameter`` one of occupancy, x, y, z and uiso.
|
|
1555
|
+
|
|
1556
|
+
``atoms`` is the driver's atom table with esds, a parameter with no esd
|
|
1557
|
+
being one not refined, and ``start`` the atoms where the job found them.
|
|
1558
|
+
Undetermined are an occupancy whose esd is more than half of
|
|
1559
|
+
:data:`OCCUPANCY_RANGE`, and a coordinate or an isotropic Uiso whose esd
|
|
1560
|
+
is larger than its shift from ``start`` (whole cells aside); an atom not
|
|
1561
|
+
in ``start`` is judged on its occupancy alone.
|
|
1562
|
+
"""
|
|
1563
|
+
before = {entry["label"]: entry for entry in start or []}
|
|
1564
|
+
half = 0.5 * (OCCUPANCY_RANGE[1] - OCCUPANCY_RANGE[0])
|
|
1565
|
+
found = []
|
|
1566
|
+
|
|
1567
|
+
def add(label, parameter, value, esd, message):
|
|
1568
|
+
found.append(
|
|
1569
|
+
{
|
|
1570
|
+
"atom": label,
|
|
1571
|
+
"parameter": parameter,
|
|
1572
|
+
"value": value,
|
|
1573
|
+
"esd": esd,
|
|
1574
|
+
"message": message,
|
|
1575
|
+
}
|
|
1576
|
+
)
|
|
1577
|
+
|
|
1578
|
+
for atom in atoms:
|
|
1579
|
+
label = atom["label"]
|
|
1580
|
+
esd = atom.get("occupancy_esd")
|
|
1581
|
+
if esd is not None and esd > half:
|
|
1582
|
+
add(
|
|
1583
|
+
label,
|
|
1584
|
+
"occupancy",
|
|
1585
|
+
atom["occupancy"],
|
|
1586
|
+
esd,
|
|
1587
|
+
f"{label} occupancy {atom['occupancy']:.3f}, esd {esd:.3f} more "
|
|
1588
|
+
f"than half its range",
|
|
1589
|
+
)
|
|
1590
|
+
then = before.get(label)
|
|
1591
|
+
if then is None:
|
|
1592
|
+
continue
|
|
1593
|
+
esds = atom.get("xyz_esd") or [None, None, None]
|
|
1594
|
+
for axis, (value, esd, was) in enumerate(zip(atom["xyz"], esds, then["xyz"])):
|
|
1595
|
+
shift = abs(_wrapped(value - was))
|
|
1596
|
+
if esd is not None and esd > shift:
|
|
1597
|
+
add(
|
|
1598
|
+
label,
|
|
1599
|
+
AXES[axis],
|
|
1600
|
+
value,
|
|
1601
|
+
esd,
|
|
1602
|
+
f"{label} {AXES[axis]} {value:.5f}, esd {esd:.5f} more than its "
|
|
1603
|
+
f"shift {shift:.5f}",
|
|
1604
|
+
)
|
|
1605
|
+
esd = atom.get("uiso_esd")
|
|
1606
|
+
if (
|
|
1607
|
+
esd is not None
|
|
1608
|
+
and atom.get("uiso") is not None
|
|
1609
|
+
and then.get("uiso") is not None
|
|
1610
|
+
):
|
|
1611
|
+
shift = abs(atom["uiso"] - then["uiso"])
|
|
1612
|
+
if esd > shift:
|
|
1613
|
+
add(
|
|
1614
|
+
label,
|
|
1615
|
+
"uiso",
|
|
1616
|
+
atom["uiso"],
|
|
1617
|
+
esd,
|
|
1618
|
+
f"{label} Uiso {atom['uiso']:.5f}, esd {esd:.5f} more than its "
|
|
1619
|
+
f"shift {shift:.5f}",
|
|
1620
|
+
)
|
|
1621
|
+
return found
|
|
1622
|
+
|
|
1623
|
+
|
|
1624
|
+
def check_policies(job):
|
|
1625
|
+
"""A refine job's ``on_flagged`` and ``on_unsettled``, each "accept" or
|
|
1626
|
+
"reject", their defaults filled in.
|
|
1627
|
+
|
|
1628
|
+
Raises
|
|
1629
|
+
------
|
|
1630
|
+
ValueError
|
|
1631
|
+
If either is anything else.
|
|
1632
|
+
"""
|
|
1633
|
+
found = []
|
|
1634
|
+
for key, default in (
|
|
1635
|
+
("on_flagged", DEFAULT_ON_FLAGGED),
|
|
1636
|
+
("on_unsettled", DEFAULT_ON_UNSETTLED),
|
|
1637
|
+
):
|
|
1638
|
+
value = job.get(key) or default
|
|
1639
|
+
if value not in POLICIES:
|
|
1640
|
+
raise ValueError(
|
|
1641
|
+
f"{key} must be one of {', '.join(POLICIES)}, not {value!r}"
|
|
1642
|
+
)
|
|
1643
|
+
found.append(value)
|
|
1644
|
+
return tuple(found)
|
|
1645
|
+
|
|
1646
|
+
|
|
1647
|
+
def set_broadening(project, histogram, broadening):
|
|
1648
|
+
"""Set isotropic crystallite size and microstrain, held fixed.
|
|
1649
|
+
|
|
1650
|
+
``broadening`` maps a phase name, or ``"*"`` for every phase, to any of
|
|
1651
|
+
``size`` in microns, ``mustrain``, and ``lgmix``, the Lorentzian fraction
|
|
1652
|
+
of both. The values are written into the phase's data for ``histogram``
|
|
1653
|
+
directly, since ``set_HAP_refinements`` can set a size but not a
|
|
1654
|
+
microstrain.
|
|
1655
|
+
"""
|
|
1656
|
+
broadening = check_broadening(broadening)
|
|
1657
|
+
phases = project.phases()
|
|
1658
|
+
missing = sorted(set(broadening) - {phase.name for phase in phases} - {ALL_PHASES})
|
|
1659
|
+
if missing:
|
|
1660
|
+
raise ValueError(f"no phases named {missing} in the project")
|
|
1661
|
+
for phase in phases:
|
|
1662
|
+
values = broadening.get(phase.name, broadening.get(ALL_PHASES))
|
|
1663
|
+
if not values:
|
|
1664
|
+
continue
|
|
1665
|
+
hap = phase.data["Histograms"][histogram.name]
|
|
1666
|
+
for key, entry in (("size", hap["Size"]), ("mustrain", hap["Mustrain"])):
|
|
1667
|
+
if key in values:
|
|
1668
|
+
entry[0] = "isotropic"
|
|
1669
|
+
entry[1][0] = values[key]
|
|
1670
|
+
entry[2][0] = False
|
|
1671
|
+
if "lgmix" in values:
|
|
1672
|
+
entry[1][2] = values["lgmix"]
|
|
1673
|
+
entry[2][2] = False
|
|
1674
|
+
|
|
1675
|
+
|
|
1676
|
+
def _broadening(phase, histogram, esds):
|
|
1677
|
+
"""Size and microstrain, with the esd of an isotropic value refined."""
|
|
1678
|
+
hap = phase.data["Histograms"][histogram.name]
|
|
1679
|
+
pfx = f"{phase.id}:{histogram.id}:"
|
|
1680
|
+
broadening = {}
|
|
1681
|
+
for key, name in (("size", "Size"), ("mustrain", "Mustrain")):
|
|
1682
|
+
entry = hap[name]
|
|
1683
|
+
broadening[key] = {
|
|
1684
|
+
"type": entry[0],
|
|
1685
|
+
"value": entry[1][0],
|
|
1686
|
+
"esd": esds.get(f"{pfx}{name};i"),
|
|
1687
|
+
"lorentzian_fraction": entry[1][2],
|
|
1688
|
+
}
|
|
1689
|
+
return broadening
|
|
1690
|
+
|
|
1691
|
+
|
|
1692
|
+
def _selected(phases, name):
|
|
1693
|
+
return ALL_PHASES in phases or name in phases
|
|
1694
|
+
|
|
1695
|
+
|
|
1696
|
+
def apply_flags(project, histogram, flags):
|
|
1697
|
+
"""Switch off every flag the stages control, then switch on ``flags``.
|
|
1698
|
+
|
|
1699
|
+
Each ``set_refinements`` and ``clear_refinements`` call carries a single
|
|
1700
|
+
key: GSASIIscriptable returns from either as soon as it has handled a
|
|
1701
|
+
``Background`` given as a bool, dropping any keys after it.
|
|
1702
|
+
"""
|
|
1703
|
+
phases = project.phases()
|
|
1704
|
+
names = {phase.name for phase in phases}
|
|
1705
|
+
named = [
|
|
1706
|
+
*flags["cell"],
|
|
1707
|
+
*flags["le_bail"],
|
|
1708
|
+
*flags["phase_fractions"],
|
|
1709
|
+
*flags["size"],
|
|
1710
|
+
*flags["mustrain"],
|
|
1711
|
+
*flags["overall_uiso"],
|
|
1712
|
+
*flags["atoms"],
|
|
1713
|
+
*flags["atom_flags"],
|
|
1714
|
+
*flags["uiso_groups"],
|
|
1715
|
+
*flags["coordinates"],
|
|
1716
|
+
*flags["origin"],
|
|
1717
|
+
*(entry["phase"] for entry in flags["occupancies"]),
|
|
1718
|
+
]
|
|
1719
|
+
missing = sorted({phase for phase in named if phase != ALL_PHASES} - names)
|
|
1720
|
+
if missing:
|
|
1721
|
+
raise ValueError(
|
|
1722
|
+
f"no phases named {missing} in the project; have {sorted(names)}"
|
|
1723
|
+
)
|
|
1724
|
+
|
|
1725
|
+
instrument = histogram.InstrumentParameters
|
|
1726
|
+
sample = histogram.SampleParameters
|
|
1727
|
+
histogram.clear_refinements({"Background": True})
|
|
1728
|
+
histogram.clear_refinements(
|
|
1729
|
+
{"Instrument Parameters": [k for k in REPORTED_INSTRUMENT if k in instrument]}
|
|
1730
|
+
)
|
|
1731
|
+
histogram.clear_refinements(
|
|
1732
|
+
{"Sample Parameters": [k for k in SAMPLE_KEYS if k in sample]}
|
|
1733
|
+
)
|
|
1734
|
+
for phase in phases:
|
|
1735
|
+
phase.clear_refinements({"Cell": True})
|
|
1736
|
+
phase.clear_refinements({"LeBail": True})
|
|
1737
|
+
phase.clear_refinements({"Atoms": [atom.label for atom in phase.atoms()]})
|
|
1738
|
+
phase.clear_HAP_refinements({"Scale": True}, [histogram])
|
|
1739
|
+
phase.clear_HAP_refinements({"Size": True}, [histogram])
|
|
1740
|
+
phase.clear_HAP_refinements({"Mustrain": True}, [histogram])
|
|
1741
|
+
|
|
1742
|
+
background = flags["background"]
|
|
1743
|
+
if background:
|
|
1744
|
+
histogram.set_refinements(
|
|
1745
|
+
{
|
|
1746
|
+
"Background": {
|
|
1747
|
+
"type": background["type"],
|
|
1748
|
+
"no. coeffs": background["terms"],
|
|
1749
|
+
"refine": True,
|
|
1750
|
+
}
|
|
1751
|
+
}
|
|
1752
|
+
)
|
|
1753
|
+
parameters = (["Zero"] if flags["zero"] else []) + flags["instrument"]
|
|
1754
|
+
if parameters:
|
|
1755
|
+
histogram.set_refinements({"Instrument Parameters": parameters})
|
|
1756
|
+
sample_flags = ["Scale"] if flags["scale"] else []
|
|
1757
|
+
if flags["displacement"]:
|
|
1758
|
+
geometry = sample.get("Type", "Bragg-Brentano")
|
|
1759
|
+
if geometry not in DISPLACEMENT:
|
|
1760
|
+
raise ValueError(f"no displacement parameters for {geometry!r} geometry")
|
|
1761
|
+
sample_flags += DISPLACEMENT[geometry]
|
|
1762
|
+
if sample_flags:
|
|
1763
|
+
histogram.set_refinements({"Sample Parameters": sample_flags})
|
|
1764
|
+
for phase in phases:
|
|
1765
|
+
if _selected(flags["cell"], phase.name):
|
|
1766
|
+
phase.set_refinements({"Cell": True})
|
|
1767
|
+
by_label = atom_flag_strings(phase, flags)
|
|
1768
|
+
values = set(by_label.values())
|
|
1769
|
+
if len(values) == 1 and "" not in values:
|
|
1770
|
+
# Every atom alike, as GSAS-II's "all".
|
|
1771
|
+
phase.set_refinements({"Atoms": {"all": values.pop()}})
|
|
1772
|
+
elif any(by_label.values()):
|
|
1773
|
+
phase.set_refinements(
|
|
1774
|
+
{"Atoms": {label: value for label, value in by_label.items() if value}}
|
|
1775
|
+
)
|
|
1776
|
+
if _selected(flags["le_bail"], phase.name):
|
|
1777
|
+
phase.set_refinements({"LeBail": True})
|
|
1778
|
+
if _selected(flags["phase_fractions"], phase.name):
|
|
1779
|
+
phase.set_HAP_refinements({"Scale": True}, [histogram])
|
|
1780
|
+
for flag, key in (("size", "Size"), ("mustrain", "Mustrain")):
|
|
1781
|
+
if _selected(flags[flag], phase.name):
|
|
1782
|
+
phase.set_HAP_refinements(
|
|
1783
|
+
{key: {"type": "isotropic", "refine": True}}, [histogram]
|
|
1784
|
+
)
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
# Refinement
|
|
1788
|
+
|
|
1789
|
+
|
|
1790
|
+
class RefinementError(RuntimeError):
|
|
1791
|
+
"""A refinement that GSAS-II reported as failed without raising."""
|
|
1792
|
+
|
|
1793
|
+
|
|
1794
|
+
def _copy_into(source, target):
|
|
1795
|
+
"""Make ``target`` a deep copy of ``source`` in place.
|
|
1796
|
+
|
|
1797
|
+
As GSASIIscriptable's reload does, every dict, and every list of the same
|
|
1798
|
+
length, that both have at the same place is kept and filled in rather
|
|
1799
|
+
than replaced, so that the objects GSASIIscriptable hands out, which hold
|
|
1800
|
+
parts of a project's data, stay valid.
|
|
1801
|
+
"""
|
|
1802
|
+
if isinstance(source, dict):
|
|
1803
|
+
for key in [key for key in target if key not in source]:
|
|
1804
|
+
del target[key]
|
|
1805
|
+
items = source.items()
|
|
1806
|
+
else:
|
|
1807
|
+
if len(source) != len(target):
|
|
1808
|
+
target[:] = copy.deepcopy(source)
|
|
1809
|
+
return
|
|
1810
|
+
items = enumerate(source)
|
|
1811
|
+
for key, value in items:
|
|
1812
|
+
current = target[key] if isinstance(source, list) or key in target else None
|
|
1813
|
+
if type(value) is type(current) and isinstance(value, (dict, list)):
|
|
1814
|
+
_copy_into(value, current)
|
|
1815
|
+
else:
|
|
1816
|
+
target[key] = copy.deepcopy(value)
|
|
1817
|
+
|
|
1818
|
+
|
|
1819
|
+
def _snapshot(project, covariance, state):
|
|
1820
|
+
"""The project's data and the driver's own bookkeeping, ``state``, as a
|
|
1821
|
+
stage kept leaves them, to go back to."""
|
|
1822
|
+
return {
|
|
1823
|
+
"data": copy.deepcopy(project.data),
|
|
1824
|
+
"covariance": covariance is not None,
|
|
1825
|
+
"state": copy.deepcopy(state),
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
|
|
1829
|
+
def _restore(project, snapshot):
|
|
1830
|
+
"""Take the project back to ``snapshot`` and save it so; the covariance
|
|
1831
|
+
of the stage kept, if there was one, and a copy of the bookkeeping."""
|
|
1832
|
+
_copy_into(snapshot["data"], project.data)
|
|
1833
|
+
project.save()
|
|
1834
|
+
covariance = project.data["Covariance"]["data"] if snapshot["covariance"] else None
|
|
1835
|
+
return covariance, copy.deepcopy(snapshot["state"])
|
|
1836
|
+
|
|
1837
|
+
|
|
1838
|
+
def _unsettled_reason(passes):
|
|
1839
|
+
last = passes[-1]
|
|
1840
|
+
reason = f"not settled in {len(passes)} passes"
|
|
1841
|
+
if last["max_shift_over_esd"] is not None:
|
|
1842
|
+
reason += (
|
|
1843
|
+
f": {last['parameter']} still moved {last['max_shift_over_esd']:.2f} "
|
|
1844
|
+
"esd in the last"
|
|
1845
|
+
)
|
|
1846
|
+
return reason
|
|
1847
|
+
|
|
1848
|
+
|
|
1849
|
+
class _Tee(io.StringIO):
|
|
1850
|
+
"""Copies what is printed while it is in force, still printing it."""
|
|
1851
|
+
|
|
1852
|
+
def __enter__(self):
|
|
1853
|
+
self._stdout = sys.stdout
|
|
1854
|
+
sys.stdout = self
|
|
1855
|
+
return self
|
|
1856
|
+
|
|
1857
|
+
def __exit__(self, *exc):
|
|
1858
|
+
sys.stdout = self._stdout
|
|
1859
|
+
return False
|
|
1860
|
+
|
|
1861
|
+
def write(self, text):
|
|
1862
|
+
self._stdout.write(text)
|
|
1863
|
+
return super().write(text)
|
|
1864
|
+
|
|
1865
|
+
|
|
1866
|
+
def _failure_report(output):
|
|
1867
|
+
lines = [line.strip() for line in output.splitlines() if line.strip()]
|
|
1868
|
+
flagged = [line for line in lines if FAILURE_WORDS.search(line)]
|
|
1869
|
+
return "\n".join((flagged or lines)[-FAILURE_LINES:])
|
|
1870
|
+
|
|
1871
|
+
|
|
1872
|
+
def _refine(project):
|
|
1873
|
+
"""Refine with the flags as set, and return the covariance data.
|
|
1874
|
+
|
|
1875
|
+
GSAS-II catches the errors of a refinement and returns without saving
|
|
1876
|
+
it, so ``G2Project.refine`` returns normally either way. The covariance
|
|
1877
|
+
is cleared first so that a refinement that saved nothing shows as one
|
|
1878
|
+
without it; the previous covariance is then put back.
|
|
1879
|
+
|
|
1880
|
+
Raises
|
|
1881
|
+
------
|
|
1882
|
+
RefinementError
|
|
1883
|
+
If the refinement saved no result, with the lines of its output that
|
|
1884
|
+
report the problem.
|
|
1885
|
+
"""
|
|
1886
|
+
covariance = project.data["Covariance"]
|
|
1887
|
+
previous = covariance["data"]
|
|
1888
|
+
covariance["data"] = {}
|
|
1889
|
+
with _Tee() as output:
|
|
1890
|
+
project.refine()
|
|
1891
|
+
covariance = project.data["Covariance"]
|
|
1892
|
+
if "Rvals" not in covariance["data"]:
|
|
1893
|
+
covariance["data"] = previous
|
|
1894
|
+
raise RefinementError(_failure_report(output.getvalue()) or "refinement failed")
|
|
1895
|
+
return covariance["data"]
|
|
1896
|
+
|
|
1897
|
+
|
|
1898
|
+
def _compute(project):
|
|
1899
|
+
"""Compute the project's pattern without refining it, so that a run
|
|
1900
|
+
that kept no stage still has a model to report and export.
|
|
1901
|
+
|
|
1902
|
+
GSAS-II works the calculated pattern out at the start of a refinement,
|
|
1903
|
+
so a refinement of no cycles saves it having moved nothing. It refines
|
|
1904
|
+
no parameter, so it leaves no covariance either, and an empty one comes
|
|
1905
|
+
back then: the values reported are the model's own, none with an esd.
|
|
1906
|
+
"""
|
|
1907
|
+
controls = project.data["Controls"]["data"]
|
|
1908
|
+
before = controls.get("cycles")
|
|
1909
|
+
previous = project.data["Covariance"]["data"]
|
|
1910
|
+
project.set_Controls("cycles", 0)
|
|
1911
|
+
try:
|
|
1912
|
+
with _Tee() as output:
|
|
1913
|
+
project.refine()
|
|
1914
|
+
if REFINED_MARK not in output.getvalue():
|
|
1915
|
+
raise RefinementError(
|
|
1916
|
+
_failure_report(output.getvalue()) or "the pattern was not computed"
|
|
1917
|
+
)
|
|
1918
|
+
covariance = project.data["Covariance"]["data"]
|
|
1919
|
+
if "Rvals" in covariance:
|
|
1920
|
+
return covariance
|
|
1921
|
+
project.data["Covariance"]["data"] = previous
|
|
1922
|
+
return {}
|
|
1923
|
+
finally:
|
|
1924
|
+
if before is not None:
|
|
1925
|
+
project.set_Controls("cycles", before)
|
|
1926
|
+
|
|
1927
|
+
|
|
1928
|
+
def _extract_le_bail(G2sc, project, cycles):
|
|
1929
|
+
"""Reset the Le Bail intensities and extract them, refining nothing.
|
|
1930
|
+
|
|
1931
|
+
As the GSAS-II GUI does when told to reset the structure factors: the
|
|
1932
|
+
project is saved with ``newLeBail`` set, so that every reflection of a Le
|
|
1933
|
+
Bail phase is given a fresh random intensity, and GSASIIstrMain.DoLeBail
|
|
1934
|
+
runs ``cycles`` extraction cycles on it and saves the project, which is
|
|
1935
|
+
then read back.
|
|
1936
|
+
|
|
1937
|
+
Raises
|
|
1938
|
+
------
|
|
1939
|
+
RefinementError
|
|
1940
|
+
If the extraction fails, with GSAS-II's message.
|
|
1941
|
+
"""
|
|
1942
|
+
random.seed(LE_BAIL_SEED)
|
|
1943
|
+
project.data["Controls"]["data"]["newLeBail"] = True
|
|
1944
|
+
project.save()
|
|
1945
|
+
with _Tee() as output:
|
|
1946
|
+
ok, rvals = G2sc.G2strMain.DoLeBail(project.filename, cycles=cycles)
|
|
1947
|
+
project.reload()
|
|
1948
|
+
if not ok:
|
|
1949
|
+
raise RefinementError(
|
|
1950
|
+
rvals.get("msg") or _failure_report(output.getvalue()) or "Le Bail failed"
|
|
1951
|
+
)
|
|
1952
|
+
return {"cycles": cycles, "rwp": rvals.get("Rwp"), "gof": rvals.get("GOF")}
|
|
1953
|
+
|
|
1954
|
+
|
|
1955
|
+
def _all_le_bail(project, le_bail):
|
|
1956
|
+
"""Whether every phase is extracted by Le Bail."""
|
|
1957
|
+
return ALL_PHASES in le_bail or {p.name for p in project.phases()} <= set(le_bail)
|
|
1958
|
+
|
|
1959
|
+
|
|
1960
|
+
def _refine_passes(project, histogram, max_passes, tolerance, ignore=()):
|
|
1961
|
+
"""Refine again and again until no parameter moves by more than
|
|
1962
|
+
``tolerance`` esds in a pass, or ``max_passes`` have been made.
|
|
1963
|
+
|
|
1964
|
+
Returns the covariance of the last pass and a record of each: Rwp, GOF
|
|
1965
|
+
and the largest shift over esd since the pass before, with the parameter
|
|
1966
|
+
that made it, leaving out those in ``ignore``.
|
|
1967
|
+
"""
|
|
1968
|
+
passes = []
|
|
1969
|
+
before = None
|
|
1970
|
+
for _ in range(max_passes):
|
|
1971
|
+
covariance = _refine(project)
|
|
1972
|
+
values = dict(zip(covariance["varyList"], covariance["variables"]))
|
|
1973
|
+
record = {
|
|
1974
|
+
"rwp": histogram.residuals.get("wR"),
|
|
1975
|
+
"gof": float(covariance["Rvals"]["GOF"]),
|
|
1976
|
+
"max_shift_over_esd": None,
|
|
1977
|
+
"parameter": None,
|
|
1978
|
+
"settled": False,
|
|
1979
|
+
}
|
|
1980
|
+
if before is not None:
|
|
1981
|
+
esds = _esds(covariance)
|
|
1982
|
+
shifts = {
|
|
1983
|
+
name: abs(value - before[name]) / esds[name]
|
|
1984
|
+
for name, value in values.items()
|
|
1985
|
+
if name in before and name not in ignore and esds.get(name)
|
|
1986
|
+
}
|
|
1987
|
+
if shifts:
|
|
1988
|
+
record["parameter"] = max(shifts, key=shifts.get)
|
|
1989
|
+
record["max_shift_over_esd"] = shifts[record["parameter"]]
|
|
1990
|
+
record["settled"] = max(shifts.values(), default=0.0) < tolerance
|
|
1991
|
+
passes.append(record)
|
|
1992
|
+
if record["settled"]:
|
|
1993
|
+
break
|
|
1994
|
+
before = values
|
|
1995
|
+
return covariance, passes
|
|
1996
|
+
|
|
1997
|
+
|
|
1998
|
+
def _esds(covariance):
|
|
1999
|
+
"""The esd of every refined parameter, and of those derived from them."""
|
|
2000
|
+
esds = {}
|
|
2001
|
+
for name, (_, esd) in covariance.get("depSigDict", {}).items():
|
|
2002
|
+
if esd is not None:
|
|
2003
|
+
esds[name] = float(esd)
|
|
2004
|
+
matrix = covariance.get("covMatrix")
|
|
2005
|
+
for index, name in enumerate(covariance.get("varyList", [])):
|
|
2006
|
+
esds[name] = float(math.sqrt(abs(matrix[index][index])))
|
|
2007
|
+
return esds
|
|
2008
|
+
|
|
2009
|
+
|
|
2010
|
+
def _stage_record(name, flags, histogram, covariance):
|
|
2011
|
+
rvals = covariance["Rvals"]
|
|
2012
|
+
esds = _esds(covariance)
|
|
2013
|
+
residuals = histogram.residuals
|
|
2014
|
+
gof = float(rvals["GOF"])
|
|
2015
|
+
return {
|
|
2016
|
+
"name": name,
|
|
2017
|
+
"flags": flags,
|
|
2018
|
+
"rwp": residuals.get("wR"),
|
|
2019
|
+
"rp": residuals.get("R"),
|
|
2020
|
+
"gof": gof,
|
|
2021
|
+
"reduced_chi_squared": gof**2,
|
|
2022
|
+
"chi_squared": float(rvals["chisq"]),
|
|
2023
|
+
"n_observations": int(rvals["Nobs"]),
|
|
2024
|
+
"n_variables": int(rvals["Nvars"]),
|
|
2025
|
+
"converged": rvals.get("converged"),
|
|
2026
|
+
# GSAS-II's "Max shft/sig": the largest shift over the whole stage,
|
|
2027
|
+
# not in its last cycle, over the esd.
|
|
2028
|
+
"max_total_shift_over_esd": rvals.get("Max shft/sig"),
|
|
2029
|
+
"messages": rvals.get("msg", ""),
|
|
2030
|
+
"parameters": {
|
|
2031
|
+
name: {"value": float(value), "esd": esds.get(name)}
|
|
2032
|
+
for name, value in zip(covariance["varyList"], covariance["variables"])
|
|
2033
|
+
},
|
|
2034
|
+
"residuals": residuals,
|
|
2035
|
+
}
|
|
2036
|
+
|
|
2037
|
+
|
|
2038
|
+
def _final(project, histogram, covariance, xinel=None):
|
|
2039
|
+
"""Instrument, sample, background and phase values after the last stage,
|
|
2040
|
+
with each phase's atoms and symmetry operators."""
|
|
2041
|
+
esds = _esds(covariance)
|
|
2042
|
+
hfx = f":{histogram.id}:"
|
|
2043
|
+
instrument = histogram.InstrumentParameters
|
|
2044
|
+
sample = histogram.SampleParameters
|
|
2045
|
+
background = histogram.Background[0]
|
|
2046
|
+
phases = []
|
|
2047
|
+
for phase in project.phases():
|
|
2048
|
+
cell, cell_esd = phase.get_cell_and_esd()
|
|
2049
|
+
pfx = f"{phase.id}:{histogram.id}:"
|
|
2050
|
+
hap = phase.getHAPvalues(histogram.name)
|
|
2051
|
+
phases.append(
|
|
2052
|
+
{
|
|
2053
|
+
"name": phase.name,
|
|
2054
|
+
"cell": cell,
|
|
2055
|
+
"cell_esd": cell_esd,
|
|
2056
|
+
"phase_fraction": {
|
|
2057
|
+
"value": float(hap["Scale"][0]),
|
|
2058
|
+
"esd": esds.get(pfx + "Scale"),
|
|
2059
|
+
},
|
|
2060
|
+
"weight_fraction": {
|
|
2061
|
+
"value": covariance.get("depSigDict", {}).get(
|
|
2062
|
+
pfx + "WgtFrac", [None]
|
|
2063
|
+
)[0],
|
|
2064
|
+
"esd": esds.get(pfx + "WgtFrac"),
|
|
2065
|
+
},
|
|
2066
|
+
"space_group": phase.data["General"].get("SGData", {}).get("SpGrp"),
|
|
2067
|
+
"le_bail": bool(hap.get("LeBail", False)),
|
|
2068
|
+
**_broadening(phase, histogram, esds),
|
|
2069
|
+
"atoms": _atom_table(phase, esds, xinel),
|
|
2070
|
+
"operators": _operators(phase),
|
|
2071
|
+
}
|
|
2072
|
+
)
|
|
2073
|
+
return {
|
|
2074
|
+
"instrument": {
|
|
2075
|
+
key: {"value": instrument[key][1], "esd": esds.get(hfx + key)}
|
|
2076
|
+
for key in REPORTED_INSTRUMENT
|
|
2077
|
+
if key in instrument
|
|
2078
|
+
},
|
|
2079
|
+
"sample": {
|
|
2080
|
+
key: {"value": sample[key][0], "esd": esds.get(hfx + key)}
|
|
2081
|
+
for key in SAMPLE_KEYS
|
|
2082
|
+
if key in sample
|
|
2083
|
+
},
|
|
2084
|
+
"background": {"type": background[0], "coefficients": background[3:]},
|
|
2085
|
+
"phases": phases,
|
|
2086
|
+
}
|
|
2087
|
+
|
|
2088
|
+
|
|
2089
|
+
# Exports
|
|
2090
|
+
|
|
2091
|
+
|
|
2092
|
+
def _export_histogram(histogram, path):
|
|
2093
|
+
import numpy as np
|
|
2094
|
+
|
|
2095
|
+
x, observed, _, calculated, background = (
|
|
2096
|
+
np.ma.getdata(column) for column in histogram.data["data"][1][:5]
|
|
2097
|
+
)
|
|
2098
|
+
lower, upper = histogram.Limits("lower"), histogram.Limits("upper")
|
|
2099
|
+
inside = (x >= lower) & (x <= upper)
|
|
2100
|
+
with open(path, "w", newline="", encoding="utf-8") as handle:
|
|
2101
|
+
writer = csv.writer(handle)
|
|
2102
|
+
writer.writerow(
|
|
2103
|
+
["two_theta", "observed", "calculated", "background", "difference"]
|
|
2104
|
+
)
|
|
2105
|
+
for row in zip(
|
|
2106
|
+
x[inside], observed[inside], calculated[inside], background[inside]
|
|
2107
|
+
):
|
|
2108
|
+
t, yo, yc, yb = (float(value) for value in row)
|
|
2109
|
+
writer.writerow(
|
|
2110
|
+
[f"{t:.6f}", f"{yo:.8g}", f"{yc:.8g}", f"{yb:.8g}", f"{yo - yc:.8g}"]
|
|
2111
|
+
)
|
|
2112
|
+
|
|
2113
|
+
|
|
2114
|
+
def _export_reflections(G2sc, reflections, path):
|
|
2115
|
+
"""One phase's reflections. ``sig`` in the list is the Gaussian variance
|
|
2116
|
+
and ``gam`` the Lorentzian FWHM, both in centidegrees."""
|
|
2117
|
+
with open(path, "w", newline="", encoding="utf-8") as handle:
|
|
2118
|
+
writer = csv.writer(handle)
|
|
2119
|
+
writer.writerow(
|
|
2120
|
+
[
|
|
2121
|
+
"h",
|
|
2122
|
+
"k",
|
|
2123
|
+
"l",
|
|
2124
|
+
"multiplicity",
|
|
2125
|
+
"d",
|
|
2126
|
+
"two_theta",
|
|
2127
|
+
"fwhm",
|
|
2128
|
+
"f_obs_squared",
|
|
2129
|
+
"f_calc_squared",
|
|
2130
|
+
"i_corr",
|
|
2131
|
+
]
|
|
2132
|
+
)
|
|
2133
|
+
for ref in reflections["RefList"]:
|
|
2134
|
+
h, k, l, multiplicity, d, two_theta, sig, gam, f_obs, f_calc = (
|
|
2135
|
+
float(value) for value in ref[:10]
|
|
2136
|
+
)
|
|
2137
|
+
fwhm = G2sc.G2pwd.getgamFW(gam, math.sqrt(max(sig, 0.0))) / 100.0
|
|
2138
|
+
writer.writerow(
|
|
2139
|
+
[
|
|
2140
|
+
round(h),
|
|
2141
|
+
round(k),
|
|
2142
|
+
round(l),
|
|
2143
|
+
round(multiplicity),
|
|
2144
|
+
f"{d:.6f}",
|
|
2145
|
+
f"{two_theta:.6f}",
|
|
2146
|
+
f"{fwhm:.6f}",
|
|
2147
|
+
f"{f_obs:.8g}",
|
|
2148
|
+
f"{f_calc:.8g}",
|
|
2149
|
+
f"{float(ref[11]):.6g}",
|
|
2150
|
+
]
|
|
2151
|
+
)
|
|
2152
|
+
|
|
2153
|
+
|
|
2154
|
+
def _safe_name(name):
|
|
2155
|
+
return re.sub(r"[^A-Za-z0-9._-]+", "_", name).strip("_") or "phase"
|
|
2156
|
+
|
|
2157
|
+
|
|
2158
|
+
def _export(G2sc, histogram, prefix):
|
|
2159
|
+
"""The pattern, the reflections of each phase and the instrument file."""
|
|
2160
|
+
prefix = Path(prefix)
|
|
2161
|
+
exports = {"histogram": str(prefix.with_name(prefix.name + "_histogram.csv"))}
|
|
2162
|
+
_export_histogram(histogram, exports["histogram"])
|
|
2163
|
+
exports["reflections"] = {}
|
|
2164
|
+
for phase, reflections in histogram.reflections().items():
|
|
2165
|
+
path = prefix.with_name(f"{prefix.name}_reflections_{_safe_name(phase)}.csv")
|
|
2166
|
+
_export_reflections(G2sc, reflections, path)
|
|
2167
|
+
exports["reflections"][phase] = str(path)
|
|
2168
|
+
exports["instprm"] = str(prefix.with_name(prefix.name + ".instprm"))
|
|
2169
|
+
instrument, extra = histogram.data["Instrument Parameters"]
|
|
2170
|
+
with open(exports["instprm"], "w", encoding="utf-8") as handle:
|
|
2171
|
+
G2sc.G2fil.WriteInstprm(handle, instrument, extra, histogram.SampleParameters)
|
|
2172
|
+
return exports
|
|
2173
|
+
|
|
2174
|
+
|
|
2175
|
+
# Actions
|
|
2176
|
+
|
|
2177
|
+
|
|
2178
|
+
def _add_histogram(project, job):
|
|
2179
|
+
"""Read the pattern, with the xrdml importer or else the .xy fallback.
|
|
2180
|
+
|
|
2181
|
+
Returns the histogram and a note of how it was read.
|
|
2182
|
+
"""
|
|
2183
|
+
data_file = job["data_file"]
|
|
2184
|
+
hint = XRDML_HINT if data_file.lower().endswith(".xrdml") else None
|
|
2185
|
+
try:
|
|
2186
|
+
histogram = project.add_powder_histogram(
|
|
2187
|
+
data_file, job["instprm"], fmthint=hint
|
|
2188
|
+
)
|
|
2189
|
+
return histogram, {"importer": "file", "file": data_file}
|
|
2190
|
+
except Exception as error:
|
|
2191
|
+
fallback = job.get("data_fallback")
|
|
2192
|
+
if not fallback:
|
|
2193
|
+
raise
|
|
2194
|
+
print(f"Import of {data_file} failed ({error!r}); reading {fallback}")
|
|
2195
|
+
histogram = project.add_powder_histogram(
|
|
2196
|
+
fallback, job["instprm"], fmthint=XY_HINT
|
|
2197
|
+
)
|
|
2198
|
+
return histogram, {
|
|
2199
|
+
"importer": "xy fallback",
|
|
2200
|
+
"file": fallback,
|
|
2201
|
+
"error": repr(error),
|
|
2202
|
+
}
|
|
2203
|
+
|
|
2204
|
+
|
|
2205
|
+
def _create_project(G2sc, job):
|
|
2206
|
+
"""A new project at ``gpx`` with the histogram and the phases linked to it.
|
|
2207
|
+
|
|
2208
|
+
A phase entry may carry ``cell``, six lattice parameters that replace
|
|
2209
|
+
those read from its CIF, and ``atoms``, edits to the occupancies and
|
|
2210
|
+
atoms read from it (:func:`check_atom_edits`), made in the project only.
|
|
2211
|
+
"""
|
|
2212
|
+
project = G2sc.G2Project(newgpx=job["gpx"])
|
|
2213
|
+
histogram, source = _add_histogram(project, job)
|
|
2214
|
+
for entry in job["phases"]:
|
|
2215
|
+
phase = project.add_phase(
|
|
2216
|
+
entry["cif"],
|
|
2217
|
+
phasename=entry["name"],
|
|
2218
|
+
histograms=[histogram],
|
|
2219
|
+
fmthint=CIF_HINT,
|
|
2220
|
+
)
|
|
2221
|
+
if entry.get("cell") is not None:
|
|
2222
|
+
cell = [float(value) for value in entry["cell"]]
|
|
2223
|
+
if len(cell) != 6:
|
|
2224
|
+
raise ValueError(f"phase {entry['name']!r}: cell needs six values")
|
|
2225
|
+
general = phase.data["General"]
|
|
2226
|
+
general["Cell"][1:7] = cell
|
|
2227
|
+
general["Cell"][7] = G2sc.G2lat.calc_V(G2sc.G2lat.cell2A(cell))
|
|
2228
|
+
if entry.get("atoms"):
|
|
2229
|
+
edit_atoms(G2sc, phase, entry["atoms"])
|
|
2230
|
+
project.save()
|
|
2231
|
+
return project, histogram, source
|
|
2232
|
+
|
|
2233
|
+
|
|
2234
|
+
def _histogram_summary(histogram):
|
|
2235
|
+
two_theta = histogram.getdata("X")
|
|
2236
|
+
instrument = histogram.InstrumentParameters
|
|
2237
|
+
sample = histogram.SampleParameters
|
|
2238
|
+
return {
|
|
2239
|
+
"name": histogram.name,
|
|
2240
|
+
"n_points": int(two_theta.size),
|
|
2241
|
+
"two_theta_range": [float(two_theta.min()), float(two_theta.max())],
|
|
2242
|
+
"limits": [histogram.Limits("lower"), histogram.Limits("upper")],
|
|
2243
|
+
# Each parameter is stored as [initial, current, refine]; the current
|
|
2244
|
+
# value is the one GSAS-II computes with.
|
|
2245
|
+
"instrument": {key: value[1] for key, value in instrument.items()},
|
|
2246
|
+
"sample": {
|
|
2247
|
+
"type": sample.get("Type"),
|
|
2248
|
+
"gonio_radius": sample.get("Gonio. radius"),
|
|
2249
|
+
},
|
|
2250
|
+
}
|
|
2251
|
+
|
|
2252
|
+
|
|
2253
|
+
def _phase_summary(phase):
|
|
2254
|
+
general = phase.data["General"]
|
|
2255
|
+
return {
|
|
2256
|
+
"name": phase.name,
|
|
2257
|
+
"space_group": general["SGData"]["SpGrp"],
|
|
2258
|
+
"cell": phase.get_cell(),
|
|
2259
|
+
"histograms": list(phase.data["Histograms"]),
|
|
2260
|
+
"atoms": _atom_table(phase),
|
|
2261
|
+
"operators": _operators(phase),
|
|
2262
|
+
}
|
|
2263
|
+
|
|
2264
|
+
|
|
2265
|
+
def create(G2sc, job):
|
|
2266
|
+
"""A new project at ``gpx``: one histogram and the phases linked to it.
|
|
2267
|
+
|
|
2268
|
+
``job`` holds ``gpx``, ``data_file``, ``instprm``, optionally
|
|
2269
|
+
``data_fallback``, and ``phases``, a list of ``{"cif": path, "name": name}``
|
|
2270
|
+
with an optional ``cell``.
|
|
2271
|
+
"""
|
|
2272
|
+
project, histogram, source = _create_project(G2sc, job)
|
|
2273
|
+
return {
|
|
2274
|
+
"gpx": project.filename,
|
|
2275
|
+
"source": source,
|
|
2276
|
+
"histogram": _histogram_summary(histogram),
|
|
2277
|
+
"phases": [_phase_summary(phase) for phase in project.phases()],
|
|
2278
|
+
}
|
|
2279
|
+
|
|
2280
|
+
|
|
2281
|
+
def _only_histogram(project, name=None):
|
|
2282
|
+
if name is not None:
|
|
2283
|
+
return project.histogram(name)
|
|
2284
|
+
histograms = project.histograms()
|
|
2285
|
+
if len(histograms) != 1:
|
|
2286
|
+
raise ValueError(
|
|
2287
|
+
f"the project has {len(histograms)} histograms; name one with 'histogram'"
|
|
2288
|
+
)
|
|
2289
|
+
return histograms[0]
|
|
2290
|
+
|
|
2291
|
+
|
|
2292
|
+
def refine(G2sc, job):
|
|
2293
|
+
"""Refine a project stage by stage and export the result.
|
|
2294
|
+
|
|
2295
|
+
``job`` holds ``gpx`` and ``stages``, and optionally ``histogram`` (the
|
|
2296
|
+
name of the one to refine, needed only if there are several),
|
|
2297
|
+
``limits`` (two theta minimum and maximum), ``cycles`` (the most least
|
|
2298
|
+
squares cycles a stage may take), ``broadening`` (sample size and
|
|
2299
|
+
microstrain to start from, held unless a stage refines them, see the
|
|
2300
|
+
module notes), ``background_start`` (``{"type": ..., "coefficients":
|
|
2301
|
+
[...]}``, the background to start from), ``overall_uiso_start`` (the
|
|
2302
|
+
Uiso an ``overall_uiso`` stage starts every atom of a phase at),
|
|
2303
|
+
``le_bail_cycles`` (the Le Bail-only cycles run when a stage switches Le
|
|
2304
|
+
Bail extraction on), ``max_passes`` and ``pass_tolerance``
|
|
2305
|
+
(refinements of each stage until it settles, see the module notes; one
|
|
2306
|
+
by default), ``scale_start`` (the histogram scale to start from),
|
|
2307
|
+
``sanity`` (the sanity check's ``max_shift`` and ``reference``, see the
|
|
2308
|
+
module notes) and ``export_prefix`` (the path the exported files are
|
|
2309
|
+
named from). With ``data_file``, ``instprm`` and ``phases`` as for
|
|
2310
|
+
``create``, the project is created first. ``on_flagged`` and
|
|
2311
|
+
``on_unsettled``, "accept" or "reject", say what becomes of a stage whose
|
|
2312
|
+
sanity check raises a new flag and of one that has not settled (see the
|
|
2313
|
+
module notes; reject and accept by default).
|
|
2314
|
+
|
|
2315
|
+
Each stage is recorded with its status, residuals, parameters, the atoms
|
|
2316
|
+
as it left them, its sanity flags, its undetermined parameters and,
|
|
2317
|
+
where it has any, the coordinates it refines and holds by site and its
|
|
2318
|
+
occupancy constraints. A stage that fails is recorded with its error and
|
|
2319
|
+
ends the run; a rejected stage is recorded with the reasons, named in the
|
|
2320
|
+
result's ``rejected``, and the run goes on from the last stage kept.
|
|
2321
|
+
|
|
2322
|
+
Every run ends with ``final`` and, with ``export_prefix``, ``exports``:
|
|
2323
|
+
the model of the last stage kept, or, where no stage was kept, the one
|
|
2324
|
+
the job started from, computed without refining it. ``final_from`` names
|
|
2325
|
+
the stage they come from, or "job start".
|
|
2326
|
+
"""
|
|
2327
|
+
raw = copy.deepcopy(job["stages"])
|
|
2328
|
+
stages = accumulate_stages(raw)
|
|
2329
|
+
on_flagged, on_unsettled = check_policies(job)
|
|
2330
|
+
broadening = check_broadening(job.get("broadening") or {})
|
|
2331
|
+
uiso_start = check_uiso_start(job.get("overall_uiso_start") or {})
|
|
2332
|
+
background_start = check_background_start(job.get("background_start"))
|
|
2333
|
+
scale_start = check_scale_start(job.get("scale_start"))
|
|
2334
|
+
sanity = check_sanity_settings(job.get("sanity"))
|
|
2335
|
+
if job.get("data_file"):
|
|
2336
|
+
project, histogram, source = _create_project(G2sc, job)
|
|
2337
|
+
else:
|
|
2338
|
+
project = G2sc.G2Project(job["gpx"])
|
|
2339
|
+
histogram, source = _only_histogram(project, job.get("histogram")), None
|
|
2340
|
+
if job.get("cycles"):
|
|
2341
|
+
project.set_Controls("cycles", int(job["cycles"]))
|
|
2342
|
+
if job.get("limits"):
|
|
2343
|
+
lower, upper = (float(value) for value in job["limits"])
|
|
2344
|
+
histogram.set_refinements({"Limits": [lower, upper]})
|
|
2345
|
+
if broadening:
|
|
2346
|
+
set_broadening(project, histogram, broadening)
|
|
2347
|
+
if background_start:
|
|
2348
|
+
coefficients = background_start["coefficients"]
|
|
2349
|
+
histogram.set_refinements(
|
|
2350
|
+
{
|
|
2351
|
+
"Background": {
|
|
2352
|
+
"type": background_start["type"],
|
|
2353
|
+
"no. coeffs": len(coefficients),
|
|
2354
|
+
"coeffs": coefficients,
|
|
2355
|
+
"refine": False,
|
|
2356
|
+
}
|
|
2357
|
+
}
|
|
2358
|
+
)
|
|
2359
|
+
if scale_start is not None:
|
|
2360
|
+
histogram.SampleParameters["Scale"][0] = scale_start
|
|
2361
|
+
|
|
2362
|
+
spc = getattr(G2sc, "G2spc", None)
|
|
2363
|
+
xinel = spc.GetCSxinel if spc else None
|
|
2364
|
+
start_atoms = {phase.name: _atom_table(phase) for phase in project.phases()}
|
|
2365
|
+
reference = sanity["reference"] or start_atoms
|
|
2366
|
+
result = {
|
|
2367
|
+
"gpx": project.filename,
|
|
2368
|
+
"source": source,
|
|
2369
|
+
"histogram": histogram.name,
|
|
2370
|
+
"limits": None,
|
|
2371
|
+
"stages": [],
|
|
2372
|
+
"completed": False,
|
|
2373
|
+
"sanity": {
|
|
2374
|
+
"max_shift": sanity["max_shift"],
|
|
2375
|
+
"reference": "given" if sanity["reference"] else "job start",
|
|
2376
|
+
},
|
|
2377
|
+
"on_flagged": on_flagged,
|
|
2378
|
+
"on_unsettled": on_unsettled,
|
|
2379
|
+
"rejected": [],
|
|
2380
|
+
}
|
|
2381
|
+
le_bail_cycles = int(job.get("le_bail_cycles") or DEFAULT_LE_BAIL_CYCLES)
|
|
2382
|
+
max_passes = int(job.get("max_passes") or 1)
|
|
2383
|
+
tolerance = float(job.get("pass_tolerance") or DEFAULT_PASS_TOLERANCE)
|
|
2384
|
+
|
|
2385
|
+
def sanity_flags(atoms_by_phase):
|
|
2386
|
+
return [
|
|
2387
|
+
{**flag, "phase": name}
|
|
2388
|
+
for name, atoms in atoms_by_phase.items()
|
|
2389
|
+
for flag in check_sanity(atoms, reference.get(name), sanity["max_shift"])
|
|
2390
|
+
]
|
|
2391
|
+
|
|
2392
|
+
def flag_keys(flags):
|
|
2393
|
+
return {(flag["phase"], flag["kind"], tuple(flag["atoms"])) for flag in flags}
|
|
2394
|
+
|
|
2395
|
+
covariance = None
|
|
2396
|
+
le_bail = []
|
|
2397
|
+
# The Uiso constraints in force for each phase, as the scheme that set
|
|
2398
|
+
# them ("overall" or the groups) and the constraints added; and the
|
|
2399
|
+
# coordinate and occupancy constraints, by phase and kind.
|
|
2400
|
+
uiso_schemes = {}
|
|
2401
|
+
managed = {}
|
|
2402
|
+
# The last stage kept, and the flags a stage's own are judged against,
|
|
2403
|
+
# at first those of the atoms as the job found them.
|
|
2404
|
+
kept = None
|
|
2405
|
+
kept_flags = flag_keys(sanity_flags(start_atoms))
|
|
2406
|
+
# The project and the bookkeeping as the last stage kept left them, to go
|
|
2407
|
+
# back to when a stage is rejected or fails.
|
|
2408
|
+
clean = _snapshot(project, covariance, (le_bail, uiso_schemes, managed))
|
|
2409
|
+
# Whatever a stage raises is recorded in the result, which is written
|
|
2410
|
+
# regardless, rather than lost with the run; the same goes for the final
|
|
2411
|
+
# values and the exports.
|
|
2412
|
+
index = 0
|
|
2413
|
+
while index < len(stages):
|
|
2414
|
+
stage = stages[index]
|
|
2415
|
+
index += 1
|
|
2416
|
+
extraction = None
|
|
2417
|
+
notes = {}
|
|
2418
|
+
try:
|
|
2419
|
+
apply_flags(project, histogram, stage["flags"])
|
|
2420
|
+
for phase in project.phases():
|
|
2421
|
+
_set_uiso_scheme(
|
|
2422
|
+
project, phase, stage["flags"], uiso_start, uiso_schemes
|
|
2423
|
+
)
|
|
2424
|
+
notes[phase.name] = set_structure_constraints(
|
|
2425
|
+
project, phase, stage["flags"], xinel, managed
|
|
2426
|
+
)
|
|
2427
|
+
if any(phase not in le_bail for phase in stage["flags"]["le_bail"]):
|
|
2428
|
+
extraction = _extract_le_bail(G2sc, project, le_bail_cycles)
|
|
2429
|
+
le_bail = stage["flags"]["le_bail"]
|
|
2430
|
+
if not job.get("max_passes"):
|
|
2431
|
+
covariance, passes = _refine(project), None
|
|
2432
|
+
else:
|
|
2433
|
+
ignore = set()
|
|
2434
|
+
if _all_le_bail(project, le_bail):
|
|
2435
|
+
ignore.add(f":{histogram.id}:Scale")
|
|
2436
|
+
covariance, passes = _refine_passes(
|
|
2437
|
+
project, histogram, max_passes, tolerance, ignore
|
|
2438
|
+
)
|
|
2439
|
+
except Exception as error: # noqa: BLE001
|
|
2440
|
+
result["stages"].append(
|
|
2441
|
+
{
|
|
2442
|
+
**stage,
|
|
2443
|
+
"status": "failed",
|
|
2444
|
+
"error": f"{type(error).__name__}: {error}",
|
|
2445
|
+
}
|
|
2446
|
+
)
|
|
2447
|
+
covariance, (le_bail, uiso_schemes, managed) = _restore(project, clean)
|
|
2448
|
+
break
|
|
2449
|
+
record = _stage_record(stage["name"], stage["flags"], histogram, covariance)
|
|
2450
|
+
# The atoms as this stage leaves them: GSAS-II refines coordinate
|
|
2451
|
+
# shifts, set back to zero after each refinement, so the parameters
|
|
2452
|
+
# alone do not give the positions.
|
|
2453
|
+
esds = _esds(covariance)
|
|
2454
|
+
record["atoms"] = {
|
|
2455
|
+
phase.name: _atom_table(phase, esds, xinel) for phase in project.phases()
|
|
2456
|
+
}
|
|
2457
|
+
record["sanity"] = sanity_flags(record["atoms"])
|
|
2458
|
+
record["undetermined"] = [
|
|
2459
|
+
{**entry, "phase": name}
|
|
2460
|
+
for name, atoms in record["atoms"].items()
|
|
2461
|
+
for entry in find_undetermined(atoms, start_atoms.get(name))
|
|
2462
|
+
]
|
|
2463
|
+
for key in ("coordinates", "occupancy_constraints"):
|
|
2464
|
+
found = {name: note[key] for name, note in notes.items() if key in note}
|
|
2465
|
+
if found:
|
|
2466
|
+
record[key] = found
|
|
2467
|
+
if extraction is not None:
|
|
2468
|
+
record["le_bail_extraction"] = extraction
|
|
2469
|
+
unsettled = False
|
|
2470
|
+
if passes is not None:
|
|
2471
|
+
record["passes"] = passes
|
|
2472
|
+
record["passes_converged"] = passes[-1]["settled"]
|
|
2473
|
+
unsettled = not passes[-1]["settled"]
|
|
2474
|
+
if unsettled:
|
|
2475
|
+
record["largest_remaining_move"] = {
|
|
2476
|
+
"parameter": passes[-1]["parameter"],
|
|
2477
|
+
"shift_over_esd": passes[-1]["max_shift_over_esd"],
|
|
2478
|
+
}
|
|
2479
|
+
new = [flag for flag in record["sanity"] if flag_keys([flag]) - kept_flags]
|
|
2480
|
+
reasons = []
|
|
2481
|
+
if new and on_flagged == "reject":
|
|
2482
|
+
reasons += [f"sanity check: {flag['message']}" for flag in new]
|
|
2483
|
+
if unsettled and on_unsettled == "reject":
|
|
2484
|
+
reasons.append(_unsettled_reason(passes))
|
|
2485
|
+
if reasons:
|
|
2486
|
+
record["status"] = "rejected"
|
|
2487
|
+
record["rejected_because"] = reasons
|
|
2488
|
+
result["stages"].append(record)
|
|
2489
|
+
result["rejected"].append(stage["name"])
|
|
2490
|
+
covariance, (le_bail, uiso_schemes, managed) = _restore(project, clean)
|
|
2491
|
+
# What this stage alone refined is held from here on: its entries
|
|
2492
|
+
# are dropped and the flags of the stages after it made again.
|
|
2493
|
+
dropped = {"name": stage["name"]}
|
|
2494
|
+
if raw[index - 1].get("reset"):
|
|
2495
|
+
dropped["reset"] = True
|
|
2496
|
+
raw[index - 1] = dropped
|
|
2497
|
+
stages = accumulate_stages(raw)
|
|
2498
|
+
continue
|
|
2499
|
+
record["status"] = "unsettled" if unsettled else "flagged" if new else "clean"
|
|
2500
|
+
result["stages"].append(record)
|
|
2501
|
+
kept = record
|
|
2502
|
+
kept_flags = flag_keys(record["sanity"])
|
|
2503
|
+
clean = _snapshot(project, covariance, (le_bail, uiso_schemes, managed))
|
|
2504
|
+
else:
|
|
2505
|
+
result["completed"] = True
|
|
2506
|
+
result["limits"] = [histogram.Limits("lower"), histogram.Limits("upper")]
|
|
2507
|
+
result["undetermined"] = kept["undetermined"] if kept else []
|
|
2508
|
+
result["final_from"] = kept["name"] if kept else "job start"
|
|
2509
|
+
if covariance is None:
|
|
2510
|
+
# Nothing was kept, so the project is back where the job found it.
|
|
2511
|
+
# Its pattern is computed, without refining anything, so that the
|
|
2512
|
+
# model the run leaves behind can still be reported and exported.
|
|
2513
|
+
try:
|
|
2514
|
+
covariance = _compute(project)
|
|
2515
|
+
except Exception as error: # noqa: BLE001
|
|
2516
|
+
result["compute_error"] = f"{type(error).__name__}: {error}"
|
|
2517
|
+
try:
|
|
2518
|
+
result["final"] = _final(project, histogram, covariance or {}, xinel)
|
|
2519
|
+
except Exception as error: # noqa: BLE001
|
|
2520
|
+
result["final_error"] = f"{type(error).__name__}: {error}"
|
|
2521
|
+
if job.get("export_prefix"):
|
|
2522
|
+
try:
|
|
2523
|
+
result["exports"] = _export(G2sc, histogram, job["export_prefix"])
|
|
2524
|
+
except Exception as error: # noqa: BLE001
|
|
2525
|
+
result["export_error"] = f"{type(error).__name__}: {error}"
|
|
2526
|
+
return result
|
|
2527
|
+
|
|
2528
|
+
|
|
2529
|
+
ACTIONS = {"create": create, "refine": refine}
|
|
2530
|
+
|
|
2531
|
+
|
|
2532
|
+
def main(argv):
|
|
2533
|
+
if len(argv) != 2:
|
|
2534
|
+
sys.exit("usage: gsas2_driver.py job.json")
|
|
2535
|
+
job = json.loads(Path(argv[1]).read_text(encoding="utf-8"))
|
|
2536
|
+
action = ACTIONS.get(job.get("action"))
|
|
2537
|
+
if action is None:
|
|
2538
|
+
sys.exit(f"unknown action {job.get('action')!r}; known: {', '.join(ACTIONS)}")
|
|
2539
|
+
|
|
2540
|
+
sys.path.append(job["gsas2_home"])
|
|
2541
|
+
from GSASII import GSASIIscriptable as G2sc
|
|
2542
|
+
|
|
2543
|
+
result = action(G2sc, job)
|
|
2544
|
+
result["action"] = job["action"]
|
|
2545
|
+
Path(job["result"]).write_text(
|
|
2546
|
+
json.dumps(_plain(result), indent=2), encoding="utf-8"
|
|
2547
|
+
)
|
|
2548
|
+
|
|
2549
|
+
|
|
2550
|
+
if __name__ == "__main__":
|
|
2551
|
+
main(sys.argv)
|