zebsdsolve 0.1.0__py3-none-win_amd64.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.
- zebsdsolve/__init__.py +669 -0
- zebsdsolve/__main__.py +3 -0
- zebsdsolve/_lib/wgpu-native-LICENSE.MIT +21 -0
- zebsdsolve/_lib/wgpu_native.dll +0 -0
- zebsdsolve/_lib/zebsdsolve.dll +0 -0
- zebsdsolve/_native.py +165 -0
- zebsdsolve/api.py +126 -0
- zebsdsolve/app.py +123 -0
- zebsdsolve/web/css/app.css +259 -0
- zebsdsolve/web/index.html +271 -0
- zebsdsolve/web/js/app.js +1341 -0
- zebsdsolve/web/js/draw.js +115 -0
- zebsdsolve/web/js/fields.js +208 -0
- zebsdsolve/web/js/votes.js +394 -0
- zebsdsolve/web/zebsdsolve/zebsdsolve.js +1056 -0
- zebsdsolve/web/zebsdsolve/zebsdsolve.wasm +0 -0
- zebsdsolve-0.1.0.dist-info/METADATA +60 -0
- zebsdsolve-0.1.0.dist-info/RECORD +22 -0
- zebsdsolve-0.1.0.dist-info/WHEEL +4 -0
- zebsdsolve-0.1.0.dist-info/entry_points.txt +2 -0
- zebsdsolve-0.1.0.dist-info/licenses/LICENSE +21 -0
- zebsdsolve-0.1.0.dist-info/licenses/wgpu-native-LICENSE.MIT +21 -0
zebsdsolve/__init__.py
ADDED
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
"""zebsdsolve: EBSD pattern-centre fitting and indexing on the GPU (Zig + WGSL over WebGPU).
|
|
2
|
+
|
|
3
|
+
The plain functions in :mod:`zebsdsolve.api` (re-exported here) cover the common workflow::
|
|
4
|
+
|
|
5
|
+
import zebsdsolve as ze
|
|
6
|
+
|
|
7
|
+
result = ze.index("scan.h5", ["Ni.npz"], fit="pose", bandwidth=64)
|
|
8
|
+
result.quaternions, result.ncc, result.phase # per map cell
|
|
9
|
+
result.geometry["pc"], result.geometry["rot"] # the fitted pattern centre and detector rotation
|
|
10
|
+
|
|
11
|
+
`Solver` is the engine itself, for when the steps are driven one at a time (as the web app does):
|
|
12
|
+
open a scan, add phases, look at patterns, fit the pattern centre, fit the pose, index.
|
|
13
|
+
|
|
14
|
+
Conventions: quaternions are (w, x, y, z) and map sample directions to crystal directions;
|
|
15
|
+
Bunge angles are radians. The geometry is one pose of the detector and, for each pattern, the
|
|
16
|
+
point of the sample the beam stood on: the pattern centre (Bruker convention: x from the left
|
|
17
|
+
and y from the top in fractions of the pattern's width and height, z in fractions of its
|
|
18
|
+
height) is that pose's position, written for the map's centre. Map cells are numbered row-major.
|
|
19
|
+
"""
|
|
20
|
+
import ctypes
|
|
21
|
+
import json
|
|
22
|
+
import os
|
|
23
|
+
|
|
24
|
+
import numpy as np
|
|
25
|
+
|
|
26
|
+
from . import _native as _n
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
_lib = None
|
|
31
|
+
_wgpu = None
|
|
32
|
+
|
|
33
|
+
LAUE = {1: "-1", 2: "2/m", 3: "mmm", 4: "4/m", 5: "4/mmm", 6: "-3", 7: "-3m", 8: "6/m", 9: "6/mmm", 10: "m-3", 11: "m-3m"}
|
|
34
|
+
METHODS = ("spherical", "dictionary", "pca")
|
|
35
|
+
MAPS = {"ipf_x": 0, "ipf_y": 1, "ipf_z": 2, "ncc": 3, "nmi": 4, "phase": 5, "z": 6, "sweep": 7}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class Error(RuntimeError):
|
|
39
|
+
"""An engine call failed; the message says which and why."""
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _load():
|
|
43
|
+
global _lib, _wgpu
|
|
44
|
+
if _lib is None:
|
|
45
|
+
_lib, _wgpu = _n.load()
|
|
46
|
+
if _lib.ze_version().decode() != __version__:
|
|
47
|
+
raise Error(f"library version {_lib.ze_version().decode()} does not match the package's {__version__}")
|
|
48
|
+
return _lib
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def _f32(a):
|
|
52
|
+
return np.ascontiguousarray(a, dtype=np.float32)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def _ptr(a, t):
|
|
56
|
+
return a.ctypes.data_as(t)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
class Solver:
|
|
60
|
+
"""One engine on one GPU device. Close it (or use it as a context manager) to free the device."""
|
|
61
|
+
|
|
62
|
+
def __init__(self):
|
|
63
|
+
lib = _load()
|
|
64
|
+
self._lib = lib
|
|
65
|
+
self._e = lib.ze_create(_wgpu.encode())
|
|
66
|
+
if not self._e:
|
|
67
|
+
buf = ctypes.create_string_buffer(512)
|
|
68
|
+
n = lib.ze_create_error(buf, 512)
|
|
69
|
+
raise Error(buf.raw[:n].decode() or "engine creation failed")
|
|
70
|
+
|
|
71
|
+
def close(self):
|
|
72
|
+
if getattr(self, "_e", None):
|
|
73
|
+
self._lib.ze_destroy(self._e)
|
|
74
|
+
self._e = None
|
|
75
|
+
|
|
76
|
+
def __enter__(self):
|
|
77
|
+
return self
|
|
78
|
+
|
|
79
|
+
def __exit__(self, *exc):
|
|
80
|
+
self.close()
|
|
81
|
+
|
|
82
|
+
def __del__(self):
|
|
83
|
+
try:
|
|
84
|
+
self.close()
|
|
85
|
+
except Exception:
|
|
86
|
+
pass
|
|
87
|
+
|
|
88
|
+
# ── plumbing ──
|
|
89
|
+
|
|
90
|
+
def _error(self):
|
|
91
|
+
buf = ctypes.create_string_buffer(1024)
|
|
92
|
+
n = self._lib.ze_error(self._e, buf, 1024)
|
|
93
|
+
return buf.raw[:n].decode(errors="replace")
|
|
94
|
+
|
|
95
|
+
def _check(self, rc, what):
|
|
96
|
+
err = self._error()
|
|
97
|
+
if rc < 0 or err:
|
|
98
|
+
raise Error(f"{what}: {err or rc}")
|
|
99
|
+
return rc
|
|
100
|
+
|
|
101
|
+
@property
|
|
102
|
+
def adapter(self):
|
|
103
|
+
"""The GPU adapter's description."""
|
|
104
|
+
buf = ctypes.create_string_buffer(256)
|
|
105
|
+
n = self._lib.ze_adapter(self._e, buf, 256)
|
|
106
|
+
return buf.raw[:n].decode(errors="replace")
|
|
107
|
+
|
|
108
|
+
def events(self):
|
|
109
|
+
"""Progress events queued since the last call, as a list of dicts."""
|
|
110
|
+
out = []
|
|
111
|
+
buf = ctypes.create_string_buffer(1 << 16)
|
|
112
|
+
while True:
|
|
113
|
+
n = self._lib.ze_events(self._e, buf, len(buf))
|
|
114
|
+
if n == 0:
|
|
115
|
+
break
|
|
116
|
+
out += [json.loads(line) for line in buf.raw[:n].decode(errors="replace").splitlines() if line]
|
|
117
|
+
return out
|
|
118
|
+
|
|
119
|
+
def cancel(self):
|
|
120
|
+
"""Ask a running fit or indexing to stop at its next step."""
|
|
121
|
+
self._lib.ze_cancel(self._e)
|
|
122
|
+
|
|
123
|
+
# ── settings and geometry ──
|
|
124
|
+
|
|
125
|
+
def configure(self, **settings):
|
|
126
|
+
"""Set options by name (src/settings.zig lists them all, with what they mean)."""
|
|
127
|
+
if settings:
|
|
128
|
+
j = json.dumps(settings).encode()
|
|
129
|
+
self._check(self._lib.ze_configure(self._e, j, len(j)), "configure")
|
|
130
|
+
return self
|
|
131
|
+
|
|
132
|
+
@property
|
|
133
|
+
def settings(self):
|
|
134
|
+
"""Every option's current value."""
|
|
135
|
+
cap = 8192
|
|
136
|
+
buf = ctypes.create_string_buffer(cap)
|
|
137
|
+
n = self._lib.ze_settings(self._e, buf, cap)
|
|
138
|
+
if n > cap:
|
|
139
|
+
buf = ctypes.create_string_buffer(n)
|
|
140
|
+
n = self._lib.ze_settings(self._e, buf, n)
|
|
141
|
+
return json.loads(buf.raw[:n].decode())
|
|
142
|
+
|
|
143
|
+
@property
|
|
144
|
+
def geometry(self):
|
|
145
|
+
"""The detector geometry: a dict with pc (3), sample_tilt_deg, detector_tilt_deg, rot (3,
|
|
146
|
+
rotation vector, radians, in the detector's own axes: swing about its row direction,
|
|
147
|
+
tilt about its column direction, twist about its normal), px_um, step_x_um, step_y_um."""
|
|
148
|
+
g = _n.Geometry()
|
|
149
|
+
self._lib.ze_get_geometry(self._e, ctypes.byref(g))
|
|
150
|
+
return {
|
|
151
|
+
"pc": list(g.pc), "sample_tilt_deg": g.sample_tilt_deg, "detector_tilt_deg": g.detector_tilt_deg,
|
|
152
|
+
"rot": list(g.rot), "px_um": g.px_um, "step_x_um": g.step_x_um, "step_y_um": g.step_y_um,
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
def set_geometry(self, **fields):
|
|
156
|
+
"""Set any of the geometry's fields, e.g. ``set_geometry(pc=(0.5, 0.2, 0.5), px_um=70)``."""
|
|
157
|
+
g = _n.Geometry()
|
|
158
|
+
self._lib.ze_get_geometry(self._e, ctypes.byref(g))
|
|
159
|
+
for k, v in fields.items():
|
|
160
|
+
if k in ("pc", "rot"):
|
|
161
|
+
setattr(g, k, (ctypes.c_double * 3)(*[float(x) for x in v]))
|
|
162
|
+
elif hasattr(g, k):
|
|
163
|
+
setattr(g, k, float(v))
|
|
164
|
+
else:
|
|
165
|
+
raise Error(f"geometry has no field {k!r}")
|
|
166
|
+
self._lib.ze_set_geometry(self._e, ctypes.byref(g))
|
|
167
|
+
return self
|
|
168
|
+
|
|
169
|
+
# ── scan ──
|
|
170
|
+
|
|
171
|
+
def open(self, *paths):
|
|
172
|
+
"""Open a scan file, and any side files it needs after it. NORDIF's Pattern.dat needs its
|
|
173
|
+
Setting.txt, which is picked up from beside it when not given."""
|
|
174
|
+
paths = [str(p) for p in paths]
|
|
175
|
+
if len(paths) == 1 and paths[0].lower().endswith(".dat"):
|
|
176
|
+
side = os.path.join(os.path.dirname(paths[0]), "Setting.txt")
|
|
177
|
+
if os.path.isfile(side):
|
|
178
|
+
paths.append(side)
|
|
179
|
+
joined = "\n".join(paths).encode()
|
|
180
|
+
self._check(self._lib.ze_open_scan(self._e, joined, len(joined)), "open")
|
|
181
|
+
return self
|
|
182
|
+
|
|
183
|
+
def synthesize(self, **settings):
|
|
184
|
+
"""Make a toy scan from the phases loaded, in place of any scan open, and return its
|
|
185
|
+
answer: a dict with quaternions (rows, cols, 4), phase (rows, cols; −1: a blank cell),
|
|
186
|
+
grain (rows, cols) and geometry (the geometry it was simulated under, which is its true
|
|
187
|
+
one: set it before this call, and move it afterwards to give a fit something to find).
|
|
188
|
+
|
|
189
|
+
Keywords are settings. The microstructure, from `toy_seed`: `toy_rows` × `toy_cols`
|
|
190
|
+
cells cut into `toy_grains` grains (1: a single crystal), each with a random orientation
|
|
191
|
+
and phase; `toy_spread_deg` turns every cell a little within its grain; `toy_blank` is
|
|
192
|
+
the share of cells holding no crystal. The detector: patterns of `toy_height` ×
|
|
193
|
+
`toy_width` pixels, `toy_contrast` of whose intensity is the simulation and the rest a
|
|
194
|
+
background (flat, or a hump of `toy_background_sigma` × the width). The counting: a
|
|
195
|
+
Poisson draw per pixel at a mean of `toy_counts` electrons (0: no noise), from
|
|
196
|
+
`toy_noise_seed` (0: from `toy_seed`), so one microstructure can be counted many times."""
|
|
197
|
+
if settings:
|
|
198
|
+
self.configure(**settings)
|
|
199
|
+
st = self.settings
|
|
200
|
+
rows, cols = st["toy_rows"], st["toy_cols"]
|
|
201
|
+
n = rows * cols
|
|
202
|
+
q = np.empty(4 * n, np.float32)
|
|
203
|
+
ph = np.empty(n, np.int32)
|
|
204
|
+
gr = np.empty(n, np.int32)
|
|
205
|
+
geometry = self.geometry
|
|
206
|
+
i32p = ctypes.POINTER(ctypes.c_int32)
|
|
207
|
+
self._check(self._lib.ze_synthesize(self._e, _ptr(q, _n.f32p), ph.ctypes.data_as(i32p), gr.ctypes.data_as(i32p), n), "synthesize")
|
|
208
|
+
return {"quaternions": q.reshape(rows, cols, 4), "phase": ph.reshape(rows, cols), "grain": gr.reshape(rows, cols), "geometry": geometry}
|
|
209
|
+
|
|
210
|
+
def set_patterns(self, patterns):
|
|
211
|
+
"""Take patterns from an array: (rows, cols, h, w) or (n, h, w), uint8, uint16 or float32."""
|
|
212
|
+
a = np.asarray(patterns)
|
|
213
|
+
if a.ndim == 3:
|
|
214
|
+
a = a[None]
|
|
215
|
+
if a.ndim != 4:
|
|
216
|
+
raise Error("patterns must be (rows, cols, h, w) or (n, h, w)")
|
|
217
|
+
if a.dtype == np.uint8:
|
|
218
|
+
pixel = 0
|
|
219
|
+
elif a.dtype == np.uint16:
|
|
220
|
+
pixel = 1
|
|
221
|
+
else:
|
|
222
|
+
a = a.astype(np.float32)
|
|
223
|
+
pixel = 2
|
|
224
|
+
a = np.ascontiguousarray(a)
|
|
225
|
+
rows, cols, h, w = a.shape
|
|
226
|
+
self._check(self._lib.ze_set_patterns(self._e, a.ctypes.data, a.nbytes, pixel, rows, cols, h, w), "set_patterns")
|
|
227
|
+
return self
|
|
228
|
+
|
|
229
|
+
@property
|
|
230
|
+
def scan(self):
|
|
231
|
+
"""What the scan is: map and pattern shape, pixel type, format, what metadata it carries."""
|
|
232
|
+
s = _n.ScanInfo()
|
|
233
|
+
self._lib.ze_scan_info(self._e, ctypes.byref(s))
|
|
234
|
+
buf = ctypes.create_string_buffer(64)
|
|
235
|
+
n = self._lib.ze_scan_format(self._e, buf, 64)
|
|
236
|
+
fmt = buf.raw[:n].decode()
|
|
237
|
+
return {
|
|
238
|
+
"rows": s.rows, "cols": s.cols, "pattern_shape": (s.pat_h, s.pat_w), "dtype": ["uint8", "uint16", "float32"][s.pixel],
|
|
239
|
+
"format": fmt, "has_background": bool(s.has_background), "has_pc": bool(s.has_pc),
|
|
240
|
+
"voltage_kv": s.voltage_kv or None, "bytes": int(s.bytes),
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
def shape(self, full=False):
|
|
244
|
+
"""(height, width) of a pattern: reduced by the `binning` setting, or (`full`) as stored."""
|
|
245
|
+
out = (ctypes.c_uint32 * 2)()
|
|
246
|
+
self._check(self._lib.ze_pattern_shape(self._e, int(full), out), "pattern_shape")
|
|
247
|
+
return int(out[0]), int(out[1])
|
|
248
|
+
|
|
249
|
+
@property
|
|
250
|
+
def pattern_shape(self):
|
|
251
|
+
"""(height, width) of a pattern as the search sees it: reduced by the `binning` setting."""
|
|
252
|
+
return self.shape()
|
|
253
|
+
|
|
254
|
+
def pattern(self, cell, processed=True, full=False):
|
|
255
|
+
"""One pattern as float32 (h, w), at the reduced size (the `binning` setting) or (`full`)
|
|
256
|
+
the stored one. `processed`: True, as it is indexed (averaged with its neighbours when
|
|
257
|
+
`nlpar` is on, then filtered); False, as it is stored; "averaged", averaged with its
|
|
258
|
+
neighbours and not filtered."""
|
|
259
|
+
h, w = self.shape(full)
|
|
260
|
+
out = np.empty(h * w, np.float32)
|
|
261
|
+
stage = 2 if processed == "averaged" else int(bool(processed))
|
|
262
|
+
self._check(self._lib.ze_pattern(self._e, int(cell), stage, int(full), _ptr(out, _n.f32p), out.size), "pattern")
|
|
263
|
+
return out.reshape(h, w)
|
|
264
|
+
|
|
265
|
+
def nlpar_view(self):
|
|
266
|
+
"""What averaging with its neighbours (`nlpar`) did to the pattern `pattern` last
|
|
267
|
+
returned: a dict with its `noise` as stored (standard deviation a pixel), the share of
|
|
268
|
+
that noise the average keeps (`left`), the share of the average that is the pattern
|
|
269
|
+
itself (`own`), and `shares`, the average's shares of the stored patterns around it
|
|
270
|
+
(side × side, the pattern itself in the middle, NaN outside the map). None when the
|
|
271
|
+
pattern was not averaged."""
|
|
272
|
+
out = np.empty(3, np.float32)
|
|
273
|
+
side = self._lib.ze_nlpar_view(self._e, _ptr(out, _n.f32p), None, 0)
|
|
274
|
+
if side == 0:
|
|
275
|
+
return None
|
|
276
|
+
shares = np.empty(side * side, np.float32)
|
|
277
|
+
self._lib.ze_nlpar_view(self._e, _ptr(out, _n.f32p), _ptr(shares, _n.f32p), shares.size)
|
|
278
|
+
return {"noise": float(out[0]), "left": float(out[1]), "own": float(out[2]), "shares": shares.reshape(side, side)}
|
|
279
|
+
|
|
280
|
+
def lattice(self, n):
|
|
281
|
+
"""About `n` map cells on a lattice over the whole map (every cell when it has no more):
|
|
282
|
+
the cells the static background is averaged over, and a quick preview's."""
|
|
283
|
+
count = self._check(self._lib.ze_lattice(self._e, int(n), None, 0), "lattice")
|
|
284
|
+
out = np.empty(count, np.uint64)
|
|
285
|
+
self._check(self._lib.ze_lattice(self._e, int(n), _ptr(out, _n.u64p), count), "lattice")
|
|
286
|
+
return out
|
|
287
|
+
|
|
288
|
+
@property
|
|
289
|
+
def pixel_angle(self):
|
|
290
|
+
"""Angular width of one stored detector pixel as the master filters take it, degrees:
|
|
291
|
+
the `pixel_angle_deg` setting, or the pixel at the pattern centre when that is 0."""
|
|
292
|
+
return float(self._lib.ze_pixel_angle(self._e))
|
|
293
|
+
|
|
294
|
+
def pattern_stats(self, cells=None):
|
|
295
|
+
"""(mean, standard deviation) of the stored patterns of the given map cells (all of them
|
|
296
|
+
by default, shaped (rows, cols)): maps of either show the microstructure before indexing."""
|
|
297
|
+
s = self.scan
|
|
298
|
+
whole = cells is None
|
|
299
|
+
c = np.arange(s["rows"] * s["cols"], dtype=np.uint64) if whole else np.ascontiguousarray(cells, dtype=np.uint64).reshape(-1)
|
|
300
|
+
out = np.empty(2 * len(c), np.float32)
|
|
301
|
+
step = 4096
|
|
302
|
+
for i in range(0, len(c), step):
|
|
303
|
+
part = np.ascontiguousarray(c[i:i + step])
|
|
304
|
+
o = out[2 * i:2 * (i + len(part))]
|
|
305
|
+
self._check(self._lib.ze_pattern_stats(self._e, _ptr(part, _n.u64p), len(part), _ptr(o, _n.f32p), o.size), "pattern_stats")
|
|
306
|
+
mean, std = out[0::2], out[1::2]
|
|
307
|
+
return (mean.reshape(s["rows"], s["cols"]), std.reshape(s["rows"], s["cols"])) if whole else (mean, std)
|
|
308
|
+
|
|
309
|
+
# ── phases ──
|
|
310
|
+
|
|
311
|
+
def add_phase(self, path):
|
|
312
|
+
"""Add a candidate phase from a master pattern (ebsdsim .npz or EMsoft .h5); returns its index."""
|
|
313
|
+
p = str(path).encode()
|
|
314
|
+
return self._check(self._lib.ze_add_phase(self._e, p, len(p)), "add_phase")
|
|
315
|
+
|
|
316
|
+
def clear_phases(self):
|
|
317
|
+
self._lib.ze_clear_phases(self._e)
|
|
318
|
+
|
|
319
|
+
@property
|
|
320
|
+
def phases(self):
|
|
321
|
+
"""The candidate phases, each a dict."""
|
|
322
|
+
out = []
|
|
323
|
+
for i in range(self._lib.ze_phase_count(self._e)):
|
|
324
|
+
p = _n.PhaseInfo()
|
|
325
|
+
self._check(self._lib.ze_phase_info(self._e, i, ctypes.byref(p)), "phase_info")
|
|
326
|
+
out.append({
|
|
327
|
+
"name": p.name.decode(errors="replace"), "side": p.side, "point_group": p.point_group, "space_group": p.space_group,
|
|
328
|
+
"laue": LAUE.get(p.laue, "?"), "voltage_kv": p.voltage_kv,
|
|
329
|
+
"n_fold": p.n_fold, "mirror": bool(p.mirror), "inversion": bool(p.inversion),
|
|
330
|
+
})
|
|
331
|
+
return out
|
|
332
|
+
|
|
333
|
+
def master(self, phase=0):
|
|
334
|
+
"""The phase's master pattern as the search compares patterns with it (smoothed, and
|
|
335
|
+
filtered as the patterns are): (2, side, side), north then south."""
|
|
336
|
+
side = self.phases[phase]["side"]
|
|
337
|
+
out = np.empty(2 * side * side, np.float32)
|
|
338
|
+
self._check(self._lib.ze_master_image(self._e, phase, _ptr(out, _n.f32p), out.size), "master")
|
|
339
|
+
return out.reshape(2, side, side)
|
|
340
|
+
|
|
341
|
+
def simulate(self, quaternions, cells=None, phase=0, full=False):
|
|
342
|
+
"""Simulated patterns (n, h, w) of a phase at the given orientations, each as the map cell
|
|
343
|
+
`cells[k]` would record it (the map's reference cell, its centre, by default), at the
|
|
344
|
+
reduced size or (`full`) the stored one."""
|
|
345
|
+
q = _f32(quaternions).reshape(-1, 4)
|
|
346
|
+
n = len(q)
|
|
347
|
+
if cells is None:
|
|
348
|
+
s = self.scan
|
|
349
|
+
cells = np.full(n, (s["rows"] // 2) * s["cols"] + s["cols"] // 2 if s["rows"] else 0)
|
|
350
|
+
c = np.ascontiguousarray(cells, dtype=np.uint64).reshape(-1)
|
|
351
|
+
h, w = self.shape(full)
|
|
352
|
+
out = np.empty(n * h * w, np.float32)
|
|
353
|
+
self._check(self._lib.ze_simulate(self._e, phase, _ptr(c, _n.u64p), _ptr(q, _n.f32p), n, int(full), _ptr(out, _n.f32p), out.size), "simulate")
|
|
354
|
+
return out.reshape(n, h, w)
|
|
355
|
+
|
|
356
|
+
# ── fits ──
|
|
357
|
+
|
|
358
|
+
def fit_pc(self):
|
|
359
|
+
"""Fit the pattern centre by the voting search (settings fit_grid, pc_*). The geometry takes
|
|
360
|
+
the result; returns a dict with pc, start, score, start_score, n_patterns, n_candidates, at_border."""
|
|
361
|
+
r = _n.PcResult()
|
|
362
|
+
self._check(self._lib.ze_fit_pc(self._e, ctypes.byref(r)), "fit_pc")
|
|
363
|
+
return {
|
|
364
|
+
"pc": list(r.pc), "start": list(r.start), "score": r.score, "start_score": r.start_score,
|
|
365
|
+
"n_patterns": r.n_patterns, "n_candidates": r.n_candidates, "levels": r.levels, "at_border": bool(r.at_border),
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
def pc_scores(self):
|
|
369
|
+
"""The last search level's votes: (scores[nz, ny, nx], pcx values, pcy values, pcz values)."""
|
|
370
|
+
shape = (ctypes.c_uint32 * 3)()
|
|
371
|
+
n = self._lib.ze_pc_scores(self._e, shape, None, 0, None, 0)
|
|
372
|
+
nz, ny, nx = (int(v) for v in shape)
|
|
373
|
+
axes = np.empty(nx + ny + nz, np.float64)
|
|
374
|
+
scores = np.empty(max(n, 0), np.float32)
|
|
375
|
+
self._lib.ze_pc_scores(self._e, shape, _ptr(axes, _n.f64p), axes.size, _ptr(scores, _n.f32p), scores.size)
|
|
376
|
+
return scores.reshape(nz, ny, nx), axes[:nx], axes[nx:nx + ny], axes[nx + ny:]
|
|
377
|
+
|
|
378
|
+
def pc_levels(self):
|
|
379
|
+
"""Every grid the last pattern-centre search scored, in order: a list of dicts with
|
|
380
|
+
scores[nz, ny, nx] (the fit patterns' mean score per candidate), the candidates' x, y, z,
|
|
381
|
+
the winner (ix, iy, iz: the candidate nearest the grid's choice), walked (the grid then
|
|
382
|
+
moved onto a winner at its border), per fit pattern the candidate it scored highest
|
|
383
|
+
under (pattern_best, rows of (ix, iy, iz)), its score there (pattern_ncc) and its score
|
|
384
|
+
under every candidate (pattern_scores[n, nz, ny, nx]); and, when ballots decided
|
|
385
|
+
(`pc_vote`), their density[nz, ny, nx], the choice in candidate steps (peak: x, y, z),
|
|
386
|
+
and how many ballots were cast and how many lay near the choice (ballots, inliers)."""
|
|
387
|
+
out = []
|
|
388
|
+
for i in range(self._lib.ze_pc_level_count(self._e)):
|
|
389
|
+
shape = (ctypes.c_uint32 * 3)()
|
|
390
|
+
info = (ctypes.c_uint32 * 2)()
|
|
391
|
+
n = self._lib.ze_pc_level(self._e, i, shape, info, None, 0, None, 0, None, None, 0)
|
|
392
|
+
nz, ny, nx = (int(v) for v in shape)
|
|
393
|
+
axes = np.empty(nx + ny + nz, np.float64)
|
|
394
|
+
scores = np.empty(n, np.float32)
|
|
395
|
+
best = np.empty(1024, np.uint32)
|
|
396
|
+
ncc = np.empty(1024, np.float32)
|
|
397
|
+
self._lib.ze_pc_level(self._e, i, shape, info, _ptr(axes, _n.f64p), axes.size, _ptr(scores, _n.f32p), n, _ptr(best, _n.u32p), _ptr(ncc, _n.f32p), best.size)
|
|
398
|
+
n_pat = self._lib.ze_fit_patterns(self._e, None, None, None, None, 0) or len(best)
|
|
399
|
+
|
|
400
|
+
def split(c):
|
|
401
|
+
return np.stack([c % nx, (c // nx) % ny, c // (nx * ny)], -1)
|
|
402
|
+
|
|
403
|
+
per = np.empty(self._lib.ze_pc_level_scores(self._e, i, None, 0), np.float32)
|
|
404
|
+
self._lib.ze_pc_level_scores(self._e, i, _ptr(per, _n.f32p), per.size)
|
|
405
|
+
out.append({"scores": scores.reshape(nz, ny, nx), "x": axes[:nx], "y": axes[nx:nx + ny], "z": axes[nx + ny:],
|
|
406
|
+
"pattern_scores": per.reshape(-1, nz, ny, nx),
|
|
407
|
+
**self._pc_ballots(i, (nz, ny, nx)),
|
|
408
|
+
"winner": tuple(int(v) for v in split(np.array(int(info[0])))), "walked": bool(info[1]),
|
|
409
|
+
"pattern_best": split(best[:n_pat].astype(np.int64)), "pattern_ncc": ncc[:n_pat].copy()})
|
|
410
|
+
return out
|
|
411
|
+
|
|
412
|
+
def _pc_ballots(self, i, shape):
|
|
413
|
+
info = np.empty(5, np.float64)
|
|
414
|
+
n = self._lib.ze_pc_level_ballots(self._e, i, None, 0, _ptr(info, _n.f64p))
|
|
415
|
+
den = np.empty(n, np.float32)
|
|
416
|
+
self._lib.ze_pc_level_ballots(self._e, i, _ptr(den, _n.f32p), n, _ptr(info, _n.f64p))
|
|
417
|
+
return {"density": den.reshape(shape) if n else None, "peak": tuple(float(v) for v in info[:3]), "ballots": int(info[3]), "inliers": int(info[4])}
|
|
418
|
+
|
|
419
|
+
def fit_pose(self):
|
|
420
|
+
"""Fit the detector's pose and the fit patterns' orientations together (settings pose_grid,
|
|
421
|
+
pose_iters): the whole pose, six degrees of freedom. Any of them can be held with the
|
|
422
|
+
settings pose_x, pose_y, pose_z (the pattern centre's coordinates) and pose_tilt,
|
|
423
|
+
pose_swing, pose_twist (the detector's turns about the source). `pose_scale` adds a
|
|
424
|
+
seventh, the scale between the beam's travel and the detector (Sim(3)): the detector's
|
|
425
|
+
pixel size. The geometry takes the result; returns a dict, its `rot` the detector's
|
|
426
|
+
swing, tilt and twist (radians) and `px_um` the pixel size."""
|
|
427
|
+
r = _n.PoseResult()
|
|
428
|
+
self._check(self._lib.ze_fit_pose(self._e, ctypes.byref(r)), "fit_pose")
|
|
429
|
+
return {
|
|
430
|
+
"pc": list(r.pc), "rot": list(r.rot), "start_pc": list(r.start_pc), "start_rot": list(r.start_rot),
|
|
431
|
+
"ncc": r.ncc, "start_ncc": r.start_ncc, "mean_turn": r.mean_turn, "px_um": r.px_um, "start_px_um": r.start_px_um,
|
|
432
|
+
"iterations": r.iterations, "accepted": r.accepted, "n_patterns": r.n_patterns, "converged": bool(r.converged),
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
def fit_patterns(self):
|
|
436
|
+
"""The patterns of the last fit: (cells, phases, quaternions (n, 4), ncc)."""
|
|
437
|
+
n = self._lib.ze_fit_patterns(self._e, None, None, None, None, 0)
|
|
438
|
+
cells = np.empty(n, np.uint64)
|
|
439
|
+
phases = np.empty(n, np.int32)
|
|
440
|
+
quats = np.empty(4 * n, np.float32)
|
|
441
|
+
ncc = np.empty(n, np.float32)
|
|
442
|
+
self._lib.ze_fit_patterns(self._e, _ptr(cells, _n.u64p), _ptr(phases, _n.i32p), _ptr(quats, _n.f32p), _ptr(ncc, _n.f32p), n)
|
|
443
|
+
return cells, phases, quats.reshape(n, 4), ncc
|
|
444
|
+
|
|
445
|
+
# ── indexing ──
|
|
446
|
+
|
|
447
|
+
def index_cells(self, cells, method=None, bandwidth=None, dict_grid=None):
|
|
448
|
+
"""Index the given map cells without storing anything: a dict of arrays shaped
|
|
449
|
+
(phases, n[, 4]): quaternions, ncc, nmi, step (the rotation of the last refinement step
|
|
450
|
+
that improved the match, radians: small once the refinement has settled) and z (how far
|
|
451
|
+
the NMI stands above chance, in chance's standard deviations). The search is the
|
|
452
|
+
settings' (`method`, `bandwidth`, `dict_grid`) unless given here."""
|
|
453
|
+
c = np.ascontiguousarray(cells, dtype=np.uint64).reshape(-1)
|
|
454
|
+
n_ph = self._lib.ze_phase_count(self._e)
|
|
455
|
+
out = np.empty(n_ph * len(c) * 8, np.float32)
|
|
456
|
+
if method is None and bandwidth is None and dict_grid is None:
|
|
457
|
+
self._check(self._lib.ze_index_cells(self._e, _ptr(c, _n.u64p), len(c), _ptr(out, _n.f32p), out.size), "index_cells")
|
|
458
|
+
else:
|
|
459
|
+
st = self.settings
|
|
460
|
+
m = METHODS.index(method if method is not None else st["method"])
|
|
461
|
+
self._check(self._lib.ze_index_cells_by(self._e, _ptr(c, _n.u64p), len(c), m, int(bandwidth or st["bandwidth"]), int(dict_grid or st["dict_grid"]),
|
|
462
|
+
_ptr(out, _n.f32p), out.size), "index_cells")
|
|
463
|
+
r = out.reshape(n_ph, len(c), 8)
|
|
464
|
+
return {"quaternions": r[..., :4].copy(), "ncc": r[..., 4].copy(), "nmi": r[..., 5].copy(), "step": r[..., 6].copy(), "z": r[..., 7].copy()}
|
|
465
|
+
|
|
466
|
+
def tune(self, progress=None):
|
|
467
|
+
"""Fit how the work is cut up to this machine and this scan. A sample of the region is
|
|
468
|
+
indexed and timed, and `batch`, `refine_pixels`, `wg_refine` and `wg_fft` each in turn
|
|
469
|
+
climb from the value they have: doubled while that is faster, else halved while that
|
|
470
|
+
is. The sample is sized by the bandwidth (8192 patterns up to 32, 4096 up to 64, 1024 up
|
|
471
|
+
to 128, 128 beyond), so a trial stays a fraction of a second: about ten trials, seconds
|
|
472
|
+
in all. Nothing stored is touched, and neither the orientations found nor which matches
|
|
473
|
+
are accepted depend on these settings. Returns the settings before and after and the
|
|
474
|
+
engine's summary. `progress(calls_done)` is called after each timed call; return False
|
|
475
|
+
from it to stop (the best so far stays)."""
|
|
476
|
+
import time
|
|
477
|
+
|
|
478
|
+
keys = ("batch", "refine_pixels", "wg_refine", "wg_fft")
|
|
479
|
+
before = {k: self.settings[k] for k in keys}
|
|
480
|
+
self._check(self._lib.ze_tune_begin(self._e), "tune")
|
|
481
|
+
done = 0
|
|
482
|
+
while True:
|
|
483
|
+
t = time.perf_counter()
|
|
484
|
+
rc = self._lib.ze_tune_trial(self._e)
|
|
485
|
+
seconds = time.perf_counter() - t
|
|
486
|
+
if rc != 0:
|
|
487
|
+
# a value this machine cannot run (out of memory, say) just loses
|
|
488
|
+
message = self._error()
|
|
489
|
+
if "Cancelled" in message:
|
|
490
|
+
raise Error("tune: " + message)
|
|
491
|
+
seconds = -1.0
|
|
492
|
+
done += 1
|
|
493
|
+
more = self._lib.ze_tune_report(self._e, seconds)
|
|
494
|
+
if not more or (progress is not None and progress(done) is False):
|
|
495
|
+
break
|
|
496
|
+
said = [e["text"] for e in self.events() if e["kind"] == "log" and e["text"].startswith("tuned on")]
|
|
497
|
+
return {"before": before, "after": {k: self.settings[k] for k in keys}, "summary": said[-1] if said else ""}
|
|
498
|
+
|
|
499
|
+
def chance(self, phase=0):
|
|
500
|
+
"""What normalized mutual information chance gives on this scan for a phase: (median,
|
|
501
|
+
spread), measured once on a sample of the region's patterns against simulations at
|
|
502
|
+
random orientations, the same for every pattern. z in the results is the NMI on that
|
|
503
|
+
scale; with `accept_nmi` 0 a match is accepted at median + accept_z × spread, and
|
|
504
|
+
`accept_nmi` above 0 is an absolute threshold in its place."""
|
|
505
|
+
out = np.empty(2, np.float32)
|
|
506
|
+
self._check(self._lib.ze_chance(self._e, int(phase), _ptr(out, _n.f32p)), "chance")
|
|
507
|
+
return float(out[0]), float(out[1])
|
|
508
|
+
|
|
509
|
+
def index_begin(self):
|
|
510
|
+
"""Start indexing the region of interest; `index_step` then does a batch at a time."""
|
|
511
|
+
self._check(self._lib.ze_index_begin(self._e), "index")
|
|
512
|
+
|
|
513
|
+
def index_step(self):
|
|
514
|
+
"""Index the next batch; returns the number of cells left to visit (0: done)."""
|
|
515
|
+
return self._check(self._lib.ze_index_step(self._e), "index")
|
|
516
|
+
|
|
517
|
+
@property
|
|
518
|
+
def index_status(self):
|
|
519
|
+
"""Where indexing stands: the sweep in progress (of `sweeps`) and what it searches with,
|
|
520
|
+
the patterns done and in it, the cells of the region and how many are accepted."""
|
|
521
|
+
st = _n.IndexStatus()
|
|
522
|
+
self._lib.ze_index_status(self._e, ctypes.byref(st))
|
|
523
|
+
out = {k: int(getattr(st, k)) for k in ("sweep", "sweeps", "bandwidth", "dict_grid", "done", "total", "region", "accepted")}
|
|
524
|
+
out["method"] = METHODS[st.method]
|
|
525
|
+
return out
|
|
526
|
+
|
|
527
|
+
@property
|
|
528
|
+
def sweep_plan(self):
|
|
529
|
+
"""What each sweep searches with, in order: a list of (bandwidth, dictionary grid, method)."""
|
|
530
|
+
out = (ctypes.c_uint32 * 3)()
|
|
531
|
+
plan = []
|
|
532
|
+
for i in range(self.settings["sweeps"]):
|
|
533
|
+
self._lib.ze_sweep(self._e, i, out)
|
|
534
|
+
plan.append((int(out[0]), int(out[1]), METHODS[out[2]]))
|
|
535
|
+
return plan
|
|
536
|
+
|
|
537
|
+
def index(self, progress=None):
|
|
538
|
+
"""Index the region of interest (settings roi_*, the whole map by default), a batch at a
|
|
539
|
+
time, in settings.sweeps sweeps. `progress(status)` is called after each batch with
|
|
540
|
+
`index_status`; return False from it to stop."""
|
|
541
|
+
self.index_begin()
|
|
542
|
+
while True:
|
|
543
|
+
left = self.index_step()
|
|
544
|
+
if progress is not None and progress(self.index_status) is False:
|
|
545
|
+
break
|
|
546
|
+
if left == 0:
|
|
547
|
+
break
|
|
548
|
+
return self.results()
|
|
549
|
+
|
|
550
|
+
def results(self):
|
|
551
|
+
"""The stored results per map cell: a dict of arrays shaped (rows, cols[, k]):
|
|
552
|
+
quaternions (4), euler (3, Bunge, radians), ncc, nmi, phase (−1: not indexed), z (how far
|
|
553
|
+
the NMI stands above chance, standard deviations) and sweep (the sweep that found an
|
|
554
|
+
accepted match, one at or above the threshold (`accept_nmi`, or `accept_z`) whose refinement had settled; −1: none did, and
|
|
555
|
+
the cell holds the best there was)."""
|
|
556
|
+
s = self.scan
|
|
557
|
+
rows, cols = s["rows"], s["cols"]
|
|
558
|
+
n = rows * cols
|
|
559
|
+
|
|
560
|
+
def get(kind, dtype, k):
|
|
561
|
+
a = np.empty(n * k, dtype)
|
|
562
|
+
self._check(self._lib.ze_results(self._e, kind, a.ctypes.data, a.nbytes), "results")
|
|
563
|
+
return a.reshape((rows, cols, k) if k > 1 else (rows, cols))
|
|
564
|
+
|
|
565
|
+
return {
|
|
566
|
+
"quaternions": get(0, np.float32, 4), "euler": get(4, np.float32, 3),
|
|
567
|
+
"ncc": get(1, np.float32, 1), "nmi": get(2, np.float32, 1), "phase": get(3, np.int32, 1),
|
|
568
|
+
"z": get(5, np.float32, 1), "sweep": get(6, np.int32, 1),
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
def map(self, kind="ipf_z"):
|
|
572
|
+
"""A result map as RGBA uint8 (rows, cols, 4): ipf_x, ipf_y, ipf_z, ncc, nmi, phase, z or sweep."""
|
|
573
|
+
s = self.scan
|
|
574
|
+
out = np.empty(s["rows"] * s["cols"] * 4, np.uint8)
|
|
575
|
+
self._check(self._lib.ze_map(self._e, MAPS[kind], _ptr(out, _n.u8p), out.size), "map")
|
|
576
|
+
return out.reshape(s["rows"], s["cols"], 4)
|
|
577
|
+
|
|
578
|
+
def ipf_key(self, phase=0, size=256, lower=False):
|
|
579
|
+
"""The key to a phase's inverse pole figure colours, as the settings have them
|
|
580
|
+
(`ipf_colors`: "classic", "hue", "perfect" or "perfect_rotation"; `ipf_lightness` and
|
|
581
|
+
`ipf_gray` for the last two): (rgba, box). `rgba` is uint8 (size, size, 4): the crystal
|
|
582
|
+
directions that stand for all the others, in stereographic projection with the crystal's
|
|
583
|
+
x to the right ([001] at the origin), transparent outside them. That is the triangle
|
|
584
|
+
between [001], [101] and [111] for the cubic classes, a wedge from the c axis for any
|
|
585
|
+
other, and twice either under a key that gives a class a light and a dark half. `box` is
|
|
586
|
+
that region's bounding box in the projection (x0, y0, x1, y1), drawn from the image's
|
|
587
|
+
lower left corner. `lower` asks for the region's part in the lower hemisphere instead,
|
|
588
|
+
which only "perfect_rotation" has, and only for the classes of a single rotation axis
|
|
589
|
+
(2/m, 4/m, 6/m, -3, -1): for the others the image is empty and the box all zero."""
|
|
590
|
+
out = np.empty(size * size * 4, np.uint8)
|
|
591
|
+
box = np.empty(6, np.float32)
|
|
592
|
+
self._check(self._lib.ze_ipf_key(self._e, int(phase), int(size), int(bool(lower)), _ptr(out, _n.u8p), out.size, _ptr(box, _n.f32p)), "ipf_key")
|
|
593
|
+
return out.reshape(size, size, 4), tuple(float(v) for v in box[:4])
|
|
594
|
+
|
|
595
|
+
def export(self, path):
|
|
596
|
+
"""Write the results as an EDAX .ang or an HKL .ctf file, by the path's extension."""
|
|
597
|
+
kind = {"ang": 0, "ctf": 1}.get(str(path).rsplit(".", 1)[-1].lower())
|
|
598
|
+
if kind is None:
|
|
599
|
+
raise Error("export writes .ang or .ctf")
|
|
600
|
+
n = self._check(self._lib.ze_export(self._e, kind, None, 0), "export")
|
|
601
|
+
buf = ctypes.create_string_buffer(n)
|
|
602
|
+
self._check(self._lib.ze_export(self._e, kind, buf, n), "export")
|
|
603
|
+
with open(path, "wb") as f:
|
|
604
|
+
f.write(buf.raw[:n])
|
|
605
|
+
|
|
606
|
+
def reduce(self, quaternions, phase=0):
|
|
607
|
+
"""Orientations as the results store them: (quaternions, euler), each quaternion's
|
|
608
|
+
representative in the phase's fundamental zone and its Bunge angles (radians). The zone
|
|
609
|
+
is that of the proper rotations of the phase's point group (for GaN, 6mm, the six about
|
|
610
|
+
c: not its Laue class's twelve, half of which turn the polar axis over)."""
|
|
611
|
+
q = _f32(quaternions).reshape(-1, 4).copy()
|
|
612
|
+
euler = np.empty((len(q), 3), np.float64)
|
|
613
|
+
for i in range(len(q)):
|
|
614
|
+
self._lib.ze_reduce(self._e, phase, _ptr(q[i:i + 1], _n.f32p), _ptr(euler[i:i + 1], _n.f64p))
|
|
615
|
+
return q, euler
|
|
616
|
+
|
|
617
|
+
def disorientation(self, a, b, phase=0):
|
|
618
|
+
"""Smallest rotation between orientations a and b (arrays of quaternions) under the
|
|
619
|
+
phase's symmetry, radians."""
|
|
620
|
+
qa = _f32(a).reshape(-1, 4)
|
|
621
|
+
qb = _f32(b).reshape(-1, 4)
|
|
622
|
+
out = np.empty(len(qa), np.float64)
|
|
623
|
+
for i in range(len(qa)):
|
|
624
|
+
out[i] = self._lib.ze_disorientation(self._e, phase, _ptr(qa[i:i + 1], _n.f32p), _ptr(qb[i:i + 1], _n.f32p))
|
|
625
|
+
return out
|
|
626
|
+
|
|
627
|
+
# ── tests and profiling ──
|
|
628
|
+
|
|
629
|
+
def _compile_wgsl(self, code, entry):
|
|
630
|
+
"""Build one kernel; None when it builds, else the compiler's message."""
|
|
631
|
+
msg = ctypes.create_string_buffer(4096)
|
|
632
|
+
c = code.encode()
|
|
633
|
+
en = entry.encode()
|
|
634
|
+
n = self._lib.ze_compile_wgsl(self._e, c, len(c), en, len(en), msg, 4096)
|
|
635
|
+
return None if n == 0 else msg.raw[:n].decode(errors="replace")
|
|
636
|
+
|
|
637
|
+
def gpu_counts(self):
|
|
638
|
+
"""GPU traffic since the last call: dispatches, submits, reads, waits."""
|
|
639
|
+
out = (ctypes.c_uint32 * 4)()
|
|
640
|
+
self._lib.ze_gpu_counts(self._e, out)
|
|
641
|
+
return dict(zip(("dispatches", "submits", "reads", "waits"), (int(v) for v in out)))
|
|
642
|
+
|
|
643
|
+
def profile(self, on=True):
|
|
644
|
+
"""Time every dispatch on the GPU from now on; `profile_report()` prints the totals."""
|
|
645
|
+
return bool(self._lib.ze_profile(self._e, int(on)))
|
|
646
|
+
|
|
647
|
+
def profile_report(self):
|
|
648
|
+
buf = ctypes.create_string_buffer(1 << 16)
|
|
649
|
+
n = self._lib.ze_profile_report(self._e, buf, len(buf))
|
|
650
|
+
return buf.raw[:n].decode()
|
|
651
|
+
|
|
652
|
+
|
|
653
|
+
def shader_modules():
|
|
654
|
+
"""The engine's WGSL modules as {name: source}, the text it compiles."""
|
|
655
|
+
lib = _load()
|
|
656
|
+
out = {}
|
|
657
|
+
i = 0
|
|
658
|
+
while True:
|
|
659
|
+
name = ctypes.create_string_buffer(64)
|
|
660
|
+
n = lib.ze_shader(i, name, 64, None, 0)
|
|
661
|
+
if n == 0:
|
|
662
|
+
return out
|
|
663
|
+
buf = ctypes.create_string_buffer(n)
|
|
664
|
+
lib.ze_shader(i, name, 64, buf, n)
|
|
665
|
+
out[name.value.decode()] = buf.raw[:n].decode()
|
|
666
|
+
i += 1
|
|
667
|
+
|
|
668
|
+
|
|
669
|
+
from .api import index, fit_pattern_center, fit_pose, load_patterns, Result # noqa: E402,F401
|
zebsdsolve/__main__.py
ADDED