midas-plotting 0.4.1__tar.gz → 0.5.0__tar.gz
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.
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/PKG-INFO +1 -1
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/__init__.py +1 -1
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/ff.py +11 -9
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/ipf.py +101 -2
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/PKG-INFO +1 -1
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/pyproject.toml +1 -1
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/tests/test_ipf.py +46 -4
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/README.md +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/cli.py +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/grains.py +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/laue.py +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/maps.py +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/mic.py +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/solutions.py +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/SOURCES.txt +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/dependency_links.txt +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/entry_points.txt +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/requires.txt +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/top_level.txt +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/setup.cfg +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/tests/test_ff.py +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/tests/test_laue.py +0 -0
- {midas_plotting-0.4.1 → midas_plotting-0.5.0}/tests/test_mic.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: midas-plotting
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Standard plots for MIDAS reconstructions - near-field, far-field and Laue: IPF maps and legends, grain maps, pole figures, strain and size distributions, and Laue texture diagnostics against their chance levels.
|
|
5
5
|
Author-email: Hemant Sharma <hsharma@anl.gov>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -48,7 +48,7 @@ from .solutions import (
|
|
|
48
48
|
)
|
|
49
49
|
from .mic import MicMap, read_mic
|
|
50
50
|
|
|
51
|
-
__version__ = "0.
|
|
51
|
+
__version__ = "0.5.0"
|
|
52
52
|
__all__ = [
|
|
53
53
|
"MicMap", "read_mic", "GrainList", "read_grains", "ff", "laue",
|
|
54
54
|
"LaueSolutions", "LaueSpots", "read_solutions", "read_spots",
|
|
@@ -26,7 +26,8 @@ from typing import Optional, Sequence
|
|
|
26
26
|
import numpy as np
|
|
27
27
|
|
|
28
28
|
from .grains import GrainList, read_grains
|
|
29
|
-
from .ipf import direction_rgb, ipf_rgb_from_matrix, laue_class, sym_matrices,
|
|
29
|
+
from .ipf import (direction_rgb, ipf_rgb_from_matrix, laue_class, sym_matrices,
|
|
30
|
+
sector_deg, triangle_corners, CUBIC)
|
|
30
31
|
|
|
31
32
|
__all__ = [
|
|
32
33
|
"ipf_legend", "grain_map", "grain_map_3d", "grain_size_distribution",
|
|
@@ -96,11 +97,13 @@ def ipf_legend(space_group: int = 225, ax=None, *, n: int = 400,
|
|
|
96
97
|
|
|
97
98
|
fam = laue_class(space_group)
|
|
98
99
|
# Sample the stereographic plane, back-project to directions, keep the ones
|
|
99
|
-
# already inside the standard triangle.
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
else
|
|
103
|
-
|
|
100
|
+
# already inside the standard triangle. Corners and the azimuthal sector
|
|
101
|
+
# come from midas_plotting.ipf, the SAME source the colour ramps fold by,
|
|
102
|
+
# so the key cannot describe a different triangle from the one drawn. The
|
|
103
|
+
# `else` branch here used to hard-code the 30 deg hexagonal triangle, which
|
|
104
|
+
# would have drawn a hexagonal key over a 4/mmm map once the tetragonal and
|
|
105
|
+
# orthorhombic triangles were added.
|
|
106
|
+
corners, labels = triangle_corners(space_group)
|
|
104
107
|
# Normalise BEFORE projecting: the stereographic map is defined on unit
|
|
105
108
|
# vectors. Projecting the raw index triple puts [111] at (0.5, 0.5)
|
|
106
109
|
# instead of (0.366, 0.366), so the corner marker and its label sit
|
|
@@ -122,7 +125,8 @@ def ipf_legend(space_group: int = 225, ax=None, *, n: int = 400,
|
|
|
122
125
|
(d[:, 1] <= d[:, 0] + 1e-9) & (d[:, 0] <= d[:, 2] + 1e-9)
|
|
123
126
|
else:
|
|
124
127
|
az = np.degrees(np.arctan2(d[:, 1], d[:, 0]))
|
|
125
|
-
inside = (d[:, 2] >= -1e-9) & (az >= -1e-9) &
|
|
128
|
+
inside = (d[:, 2] >= -1e-9) & (az >= -1e-9) & \
|
|
129
|
+
(az <= sector_deg(space_group) + 1e-9)
|
|
126
130
|
|
|
127
131
|
rgb = np.ones((d.shape[0], 3))
|
|
128
132
|
if inside.any():
|
|
@@ -132,8 +136,6 @@ def ipf_legend(space_group: int = 225, ax=None, *, n: int = 400,
|
|
|
132
136
|
|
|
133
137
|
ax.imshow(img, origin="lower", alpha=alpha,
|
|
134
138
|
extent=(x0, x1, y0, y1))
|
|
135
|
-
labels = (["[001]", "[101]", "[111]"] if fam == CUBIC
|
|
136
|
-
else ["[0001]", r"[10$\bar{1}$0]", r"[2$\bar{1}\bar{1}$0]"])
|
|
137
139
|
for (px, py), lab in zip(zip(cx, cy), labels):
|
|
138
140
|
ax.plot(px, py, "k.", ms=4)
|
|
139
141
|
ax.annotate(lab, (px, py), textcoords="offset points",
|
|
@@ -13,21 +13,35 @@ from __future__ import annotations
|
|
|
13
13
|
|
|
14
14
|
from typing import Sequence
|
|
15
15
|
|
|
16
|
+
import math
|
|
16
17
|
import numpy as np
|
|
17
18
|
|
|
18
|
-
__all__ = ["ipf_rgb", "
|
|
19
|
+
__all__ = ["ipf_rgb", "ipf_rgb_from_matrix", "direction_rgb", "laue_class",
|
|
20
|
+
"sym_matrices", "sector_deg", "triangle_corners",
|
|
21
|
+
"CUBIC", "HEXAGONAL", "TRIGONAL", "TETRAGONAL", "ORTHORHOMBIC"]
|
|
19
22
|
|
|
20
23
|
CUBIC = "cubic"
|
|
24
|
+
TETRAGONAL = "tetragonal"
|
|
25
|
+
ORTHORHOMBIC = "orthorhombic"
|
|
21
26
|
HEXAGONAL = "hexagonal"
|
|
22
27
|
TRIGONAL = "trigonal"
|
|
23
28
|
|
|
24
29
|
# Laue class per space-group range, for the ones MIDAS actually reconstructs.
|
|
25
30
|
# Deliberately explicit rather than clever: a wrong guess here silently
|
|
26
31
|
# recolours a map without any other symptom.
|
|
32
|
+
#
|
|
33
|
+
# The ranges are LAUE CLASSES, not crystal systems, and the two do not coincide
|
|
34
|
+
# in the tetragonal case: 89-142 is 4/mmm, whose triangle closes at 45 deg on
|
|
35
|
+
# [110], while 75-88 is 4/m, which has no in-plane mirror and whose triangle
|
|
36
|
+
# spans the full 90 deg to [010]. Colouring a 4/m crystal with the 4/mmm
|
|
37
|
+
# triangle folds by a 2-fold it does not have, so 75-88 is left OUT and refuses
|
|
38
|
+
# rather than guessing. Implement `_rgb_tetragonal_4m` before widening it.
|
|
27
39
|
_SG_LAUE = [
|
|
28
40
|
(195, 230, CUBIC),
|
|
29
41
|
(168, 194, HEXAGONAL),
|
|
30
42
|
(143, 167, TRIGONAL),
|
|
43
|
+
(89, 142, TETRAGONAL), # 4/mmm only -- see the note above
|
|
44
|
+
(16, 74, ORTHORHOMBIC),
|
|
31
45
|
]
|
|
32
46
|
|
|
33
47
|
|
|
@@ -43,7 +57,10 @@ def laue_class(space_group: int) -> str:
|
|
|
43
57
|
return name
|
|
44
58
|
raise NotImplementedError(
|
|
45
59
|
f"IPF colouring for space group {space_group} is not implemented "
|
|
46
|
-
f"(have: cubic 195-230, hexagonal 168-194, trigonal 143-167
|
|
60
|
+
f"(have: cubic 195-230, hexagonal 168-194, trigonal 143-167, "
|
|
61
|
+
f"tetragonal 4/mmm 89-142, orthorhombic 16-74). Refusing to guess. "
|
|
62
|
+
f"Note 75-88 is Laue class 4/m, whose triangle spans 90 deg, not the "
|
|
63
|
+
f"45 deg of 4/mmm; it is deliberately not implemented."
|
|
47
64
|
)
|
|
48
65
|
|
|
49
66
|
|
|
@@ -69,6 +86,38 @@ def _rgb_cubic(d: np.ndarray) -> np.ndarray:
|
|
|
69
86
|
return rgb
|
|
70
87
|
|
|
71
88
|
|
|
89
|
+
|
|
90
|
+
def _rgb_tetragonal(d: np.ndarray) -> np.ndarray:
|
|
91
|
+
"""Standard [001]-[100]-[110] triangle for Laue class 4/mmm.
|
|
92
|
+
|
|
93
|
+
Same construction as :func:`_rgb_hexagonal`: red is the c-axis component,
|
|
94
|
+
and the in-plane azimuth interpolates green -> blue across the triangle's
|
|
95
|
+
angular width, here 45 degrees rather than 30. Added for I4/mmm (SG 139),
|
|
96
|
+
the Ruddlesden-Popper subcell — before this the function raised, which was
|
|
97
|
+
correct: colouring a tetragonal crystal with the cubic triangle folds by a
|
|
98
|
+
3-fold the crystal does not have.
|
|
99
|
+
"""
|
|
100
|
+
dz = np.abs(d[:, 2])
|
|
101
|
+
planar = np.hypot(d[:, 0], d[:, 1])
|
|
102
|
+
phi = np.degrees(np.arctan2(np.abs(d[:, 1]), np.abs(d[:, 0])))
|
|
103
|
+
phi = np.minimum(phi % 90.0, 90.0 - (phi % 90.0)) # fold to [0, 45]
|
|
104
|
+
t = np.clip(phi / 45.0, 0.0, 1.0)
|
|
105
|
+
return np.stack([dz, planar * (1.0 - t), planar * t], axis=1)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _rgb_orthorhombic(d: np.ndarray) -> np.ndarray:
|
|
109
|
+
"""Standard [001]-[100]-[010] triangle for Laue class mmm.
|
|
110
|
+
|
|
111
|
+
The triangle spans a full 90 degrees of azimuth because mmm has no
|
|
112
|
+
in-plane rotation relating a to b. Added for Fmmm (SG 69).
|
|
113
|
+
"""
|
|
114
|
+
dz = np.abs(d[:, 2])
|
|
115
|
+
planar = np.hypot(d[:, 0], d[:, 1])
|
|
116
|
+
phi = np.degrees(np.arctan2(np.abs(d[:, 1]), np.abs(d[:, 0])))
|
|
117
|
+
t = np.clip(phi / 90.0, 0.0, 1.0)
|
|
118
|
+
return np.stack([dz, planar * (1.0 - t), planar * t], axis=1)
|
|
119
|
+
|
|
120
|
+
|
|
72
121
|
def _rgb_hexagonal(d: np.ndarray) -> np.ndarray:
|
|
73
122
|
"""Standard [0001]-[10-10]-[2-1-10] triangle.
|
|
74
123
|
|
|
@@ -233,6 +282,14 @@ def direction_rgb(
|
|
|
233
282
|
rgb = _rgb_cubic(red)
|
|
234
283
|
elif fam == TRIGONAL:
|
|
235
284
|
rgb = _rgb_trigonal(d, sym)
|
|
285
|
+
elif fam in (TETRAGONAL, ORTHORHOMBIC):
|
|
286
|
+
dd = d.copy()
|
|
287
|
+
dd[:, :, 2] = np.abs(dd[:, :, 2])
|
|
288
|
+
pick = np.argmax(dd[:, :, 2], axis=1) # closest to [001]
|
|
289
|
+
red = dd[np.arange(dd.shape[0]), pick]
|
|
290
|
+
red /= np.linalg.norm(red, axis=1, keepdims=True)
|
|
291
|
+
rgb = (_rgb_tetragonal(red) if fam == TETRAGONAL
|
|
292
|
+
else _rgb_orthorhombic(red))
|
|
236
293
|
else:
|
|
237
294
|
dd = d.copy()
|
|
238
295
|
dd[:, :, 2] = np.abs(dd[:, :, 2])
|
|
@@ -245,3 +302,45 @@ def direction_rgb(
|
|
|
245
302
|
mx = rgb.max(axis=1, keepdims=True)
|
|
246
303
|
rgb = np.where(mx > 0, rgb / mx, rgb)
|
|
247
304
|
return np.clip(rgb ** float(gamma), 0.0, 1.0)
|
|
305
|
+
|
|
306
|
+
def sector_deg(space_group: int) -> float:
|
|
307
|
+
"""Azimuthal width of the standard triangle, in degrees.
|
|
308
|
+
|
|
309
|
+
The SAME number the colour ramps fold by, so a legend built from this
|
|
310
|
+
cannot disagree with the map it explains. Cubic is excluded: its triangle
|
|
311
|
+
closes on [111], not on an in-plane direction, so an azimuth does not
|
|
312
|
+
describe it.
|
|
313
|
+
"""
|
|
314
|
+
fam = laue_class(space_group)
|
|
315
|
+
if fam == CUBIC:
|
|
316
|
+
raise ValueError("the cubic triangle is not defined by an azimuth; "
|
|
317
|
+
"its third corner is [111]")
|
|
318
|
+
if fam == TETRAGONAL:
|
|
319
|
+
return 45.0
|
|
320
|
+
if fam == ORTHORHOMBIC:
|
|
321
|
+
return 90.0
|
|
322
|
+
if fam == HEXAGONAL:
|
|
323
|
+
return 30.0
|
|
324
|
+
if fam == TRIGONAL:
|
|
325
|
+
return _trigonal_sector_deg(sym_matrices(space_group))
|
|
326
|
+
raise NotImplementedError(f"no triangle implemented for {fam}")
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
def triangle_corners(space_group: int):
|
|
330
|
+
"""``(corners, labels)`` for the standard triangle of this Laue class.
|
|
331
|
+
|
|
332
|
+
Cubic closes on [111]; every other implemented class is
|
|
333
|
+
[001] - [100] - (the in-plane direction at :func:`sector_deg`).
|
|
334
|
+
"""
|
|
335
|
+
fam = laue_class(space_group)
|
|
336
|
+
if fam == CUBIC:
|
|
337
|
+
return (np.array([[0., 0., 1.], [1., 0., 1.], [1., 1., 1.]]),
|
|
338
|
+
["[001]", "[101]", "[111]"])
|
|
339
|
+
phi = math.radians(sector_deg(space_group))
|
|
340
|
+
corners = np.array([[0., 0., 1.], [1., 0., 0.],
|
|
341
|
+
[math.cos(phi), math.sin(phi), 0.]])
|
|
342
|
+
lab = {TETRAGONAL: ["[001]", "[100]", "[110]"],
|
|
343
|
+
ORTHORHOMBIC: ["[001]", "[100]", "[010]"],
|
|
344
|
+
HEXAGONAL: ["[0001]", r"[10$\bar{1}$0]", r"[2$\bar{1}\bar{1}$0]"],
|
|
345
|
+
TRIGONAL: ["[0001]", r"[10$\bar{1}$0]", r"[01$\bar{1}$0]"]}[fam]
|
|
346
|
+
return corners, lab
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: midas-plotting
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.5.0
|
|
4
4
|
Summary: Standard plots for MIDAS reconstructions - near-field, far-field and Laue: IPF maps and legends, grain maps, pole figures, strain and size distributions, and Laue texture diagnostics against their chance levels.
|
|
5
5
|
Author-email: Hemant Sharma <hsharma@anl.gov>
|
|
6
6
|
License: BSD-3-Clause
|
|
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "midas-plotting"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.5.0"
|
|
8
8
|
description = "Standard plots for MIDAS reconstructions - near-field, far-field and Laue: IPF maps and legends, grain maps, pole figures, strain and size distributions, and Laue texture diagnostics against their chance levels."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
license = { text = "BSD-3-Clause" }
|
|
@@ -21,11 +21,53 @@ def test_laue_class_known_families():
|
|
|
21
21
|
|
|
22
22
|
|
|
23
23
|
def test_laue_class_refuses_unimplemented_rather_than_guessing():
|
|
24
|
-
"""A silent fallback to cubic would recolour a map with no other symptom.
|
|
25
|
-
|
|
26
|
-
|
|
24
|
+
"""A silent fallback to cubic would recolour a map with no other symptom.
|
|
25
|
+
|
|
26
|
+
SG 139 (tetragonal) and 69 (orthorhombic) were on this list until their
|
|
27
|
+
triangles were implemented; triclinic and monoclinic still are not, and the
|
|
28
|
+
contract is that they REFUSE rather than guess.
|
|
29
|
+
|
|
30
|
+
SG 75-88 is the subtle one. It is tetragonal by CRYSTAL SYSTEM but its Laue
|
|
31
|
+
class is 4/m, not 4/mmm: no in-plane mirror, so its triangle spans 90 deg to
|
|
32
|
+
[010] rather than 45 deg to [110]. Colouring it with the 4/mmm ramp folds by
|
|
33
|
+
a 2-fold the crystal does not have and yields a plausible, wrong map. It is
|
|
34
|
+
deliberately excluded until `_rgb_tetragonal_4m` exists.
|
|
35
|
+
"""
|
|
36
|
+
for sg in (1, 2, 5, 15): # triclinic + monoclinic
|
|
37
|
+
with pytest.raises(NotImplementedError, match="not implemented"):
|
|
38
|
+
laue_class(sg)
|
|
39
|
+
for sg in (75, 81, 88): # Laue class 4/m, NOT 4/mmm
|
|
40
|
+
with pytest.raises(NotImplementedError, match="not implemented"):
|
|
41
|
+
laue_class(sg)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def test_tetragonal_range_is_the_laue_class_not_the_crystal_system():
|
|
45
|
+
"""4/mmm starts at 89. The boundary is the whole point of the split."""
|
|
46
|
+
from midas_plotting.ipf import laue_class, sector_deg, TETRAGONAL
|
|
27
47
|
with pytest.raises(NotImplementedError):
|
|
28
|
-
laue_class(
|
|
48
|
+
laue_class(88)
|
|
49
|
+
assert laue_class(89) == TETRAGONAL
|
|
50
|
+
assert sector_deg(89) == 45.0 and sector_deg(142) == 45.0
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def test_tetragonal_and_orthorhombic_triangles_are_distinct():
|
|
54
|
+
"""4/mmm and mmm must not be the same colouring, and neither may be cubic.
|
|
55
|
+
|
|
56
|
+
The discriminator is [010]: under 4/mmm a 4-fold makes it equivalent to
|
|
57
|
+
[100] (both GREEN), while mmm has no in-plane rotation so [010] is the
|
|
58
|
+
third vertex (BLUE). A tetragonal map coloured with the mmm triangle -- or
|
|
59
|
+
with the cubic one, which folds by a 3-fold the crystal does not have --
|
|
60
|
+
would look plausible and be wrong.
|
|
61
|
+
"""
|
|
62
|
+
import numpy as np
|
|
63
|
+
from midas_plotting.ipf import direction_rgb
|
|
64
|
+
dirs = np.array([[0., 0., 1.], [1., 0., 0.], [0., 1., 0.]])
|
|
65
|
+
tet = direction_rgb(dirs, space_group=139)
|
|
66
|
+
ort = direction_rgb(dirs, space_group=69)
|
|
67
|
+
assert tet[0, 0] > 0.9 and ort[0, 0] > 0.9 # [001] is red in both
|
|
68
|
+
assert np.allclose(tet[1], tet[2], atol=1e-6), "4/mmm: [100] and [010] must match"
|
|
69
|
+
assert not np.allclose(ort[1], ort[2], atol=0.5), "mmm: [100] and [010] must differ"
|
|
70
|
+
assert ort[2, 2] > 0.9, "mmm: [010] is the blue vertex"
|
|
29
71
|
|
|
30
72
|
|
|
31
73
|
def test_sym_matrices_are_proper_rotations():
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|