midas-plotting 0.4.0__tar.gz → 0.4.1__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.0 → midas_plotting-0.4.1}/PKG-INFO +1 -1
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/__init__.py +1 -1
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/ipf.py +66 -3
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/PKG-INFO +1 -1
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/pyproject.toml +1 -1
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/tests/test_ipf.py +63 -1
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/README.md +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/cli.py +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/ff.py +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/grains.py +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/laue.py +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/maps.py +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/mic.py +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/solutions.py +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/SOURCES.txt +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/dependency_links.txt +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/entry_points.txt +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/requires.txt +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/top_level.txt +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/setup.cfg +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/tests/test_ff.py +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/tests/test_laue.py +0 -0
- {midas_plotting-0.4.0 → midas_plotting-0.4.1}/tests/test_mic.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: midas-plotting
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
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.4.
|
|
51
|
+
__version__ = "0.4.1"
|
|
52
52
|
__all__ = [
|
|
53
53
|
"MicMap", "read_mic", "GrainList", "read_grains", "ff", "laue",
|
|
54
54
|
"LaueSolutions", "LaueSpots", "read_solutions", "read_spots",
|
|
@@ -15,10 +15,11 @@ from typing import Sequence
|
|
|
15
15
|
|
|
16
16
|
import numpy as np
|
|
17
17
|
|
|
18
|
-
__all__ = ["ipf_rgb", "sym_matrices", "CUBIC", "HEXAGONAL"]
|
|
18
|
+
__all__ = ["ipf_rgb", "sym_matrices", "CUBIC", "HEXAGONAL", "TRIGONAL"]
|
|
19
19
|
|
|
20
20
|
CUBIC = "cubic"
|
|
21
21
|
HEXAGONAL = "hexagonal"
|
|
22
|
+
TRIGONAL = "trigonal"
|
|
22
23
|
|
|
23
24
|
# Laue class per space-group range, for the ones MIDAS actually reconstructs.
|
|
24
25
|
# Deliberately explicit rather than clever: a wrong guess here silently
|
|
@@ -26,6 +27,7 @@ HEXAGONAL = "hexagonal"
|
|
|
26
27
|
_SG_LAUE = [
|
|
27
28
|
(195, 230, CUBIC),
|
|
28
29
|
(168, 194, HEXAGONAL),
|
|
30
|
+
(143, 167, TRIGONAL),
|
|
29
31
|
]
|
|
30
32
|
|
|
31
33
|
|
|
@@ -41,7 +43,7 @@ def laue_class(space_group: int) -> str:
|
|
|
41
43
|
return name
|
|
42
44
|
raise NotImplementedError(
|
|
43
45
|
f"IPF colouring for space group {space_group} is not implemented "
|
|
44
|
-
f"(have: cubic 195-230, hexagonal 168-194). Refusing to guess."
|
|
46
|
+
f"(have: cubic 195-230, hexagonal 168-194, trigonal 143-167). Refusing to guess."
|
|
45
47
|
)
|
|
46
48
|
|
|
47
49
|
|
|
@@ -81,6 +83,58 @@ def _rgb_hexagonal(d: np.ndarray) -> np.ndarray:
|
|
|
81
83
|
return np.stack([dz, planar * (1.0 - t), planar * t], axis=1)
|
|
82
84
|
|
|
83
85
|
|
|
86
|
+
def _trigonal_sector_deg(sym: np.ndarray) -> float:
|
|
87
|
+
"""Azimuthal fundamental-sector width, measured from the operators.
|
|
88
|
+
|
|
89
|
+
Counts the distinct upper-hemisphere azimuths a generic direction is sent
|
|
90
|
+
to by the group plus the Laue centre. For -3m that is 6 images, so 60 deg
|
|
91
|
+
-- twice the hexagonal 30. Measured rather than hard-coded so a change in
|
|
92
|
+
the operator set cannot silently mis-scale the colour ramp.
|
|
93
|
+
"""
|
|
94
|
+
g = np.array([0.3411, 0.1297, 0.4271]) # generic, no special azimuth
|
|
95
|
+
g = g / np.linalg.norm(g)
|
|
96
|
+
both = np.concatenate([sym @ g, -(sym @ g)], axis=0)
|
|
97
|
+
up = both[both[:, 2] >= -1e-12]
|
|
98
|
+
phi = np.sort(np.degrees(np.arctan2(up[:, 1], up[:, 0])) % 360.0)
|
|
99
|
+
keep = [phi[0]]
|
|
100
|
+
for a in phi[1:]:
|
|
101
|
+
if a - keep[-1] > 1e-6:
|
|
102
|
+
keep.append(a)
|
|
103
|
+
return 360.0 / len(keep)
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
def _rgb_trigonal(d_all: np.ndarray, sym: np.ndarray) -> np.ndarray:
|
|
107
|
+
"""Standard [0001]-[10-10]-[01-10] triangle for Laue class -3m.
|
|
108
|
+
|
|
109
|
+
D3 has 6 proper rotations (3-fold about c, three 2-fold in the basal
|
|
110
|
+
plane) against 6/mmm's 12, so the sector spans 60 deg, not 30. Colouring
|
|
111
|
+
R-3m with the hexagonal triangle folds by a 6-fold axis the crystal does
|
|
112
|
+
not have and silently gives distinct orientations the same colour.
|
|
113
|
+
|
|
114
|
+
The azimuth is NOT hand-folded. Where MIDAS puts the 2-fold axes is a
|
|
115
|
+
convention, and assuming one sits at azimuth 0 gives a colouring that is
|
|
116
|
+
not symmetry invariant (measured: colour moved by up to 0.99 under
|
|
117
|
+
``g -> S.g``). Instead take the whole orbit, add ``-d`` for the Laue
|
|
118
|
+
centre, keep the upper hemisphere and pick the smallest azimuth --
|
|
119
|
+
canonical whatever the operator convention.
|
|
120
|
+
|
|
121
|
+
``d_all`` is ``(n, n_sym, 3)``, every symmetry image of each direction.
|
|
122
|
+
"""
|
|
123
|
+
both = np.concatenate([d_all, -d_all], axis=1) # (n, 2s, 3)
|
|
124
|
+
phi = np.degrees(np.arctan2(both[:, :, 1], both[:, :, 0])) % 360.0
|
|
125
|
+
phi = np.where(both[:, :, 2] >= -1e-12, phi, np.inf) # upper only
|
|
126
|
+
k = np.argmin(phi, axis=1)
|
|
127
|
+
idx = np.arange(both.shape[0])
|
|
128
|
+
rep, phi_c = both[idx, k], phi[idx, k]
|
|
129
|
+
phi_c = np.where(np.isfinite(phi_c), phi_c, 0.0)
|
|
130
|
+
rep = rep / np.linalg.norm(rep, axis=1, keepdims=True)
|
|
131
|
+
|
|
132
|
+
dz = np.abs(rep[:, 2])
|
|
133
|
+
planar = np.hypot(rep[:, 0], rep[:, 1])
|
|
134
|
+
t = np.clip(phi_c / _trigonal_sector_deg(sym), 0.0, 1.0)
|
|
135
|
+
return np.stack([dz, planar * (1.0 - t), planar * t], axis=1)
|
|
136
|
+
|
|
137
|
+
|
|
84
138
|
def ipf_rgb(
|
|
85
139
|
euler: np.ndarray,
|
|
86
140
|
space_group: int = 225,
|
|
@@ -139,7 +193,14 @@ def ipf_rgb_from_matrix(
|
|
|
139
193
|
raise ValueError("axis must be non-zero")
|
|
140
194
|
a = a / n
|
|
141
195
|
|
|
142
|
-
|
|
196
|
+
# TRANSPOSE. MIDAS orientation matrices map CRYSTAL -> LAB (v_lab = g v_crystal),
|
|
197
|
+
# so the crystal direction parallel to the sample axis `a` is g^T a, not g a.
|
|
198
|
+
# This is not cosmetic: with g a, the colour is NOT symmetry-invariant for this
|
|
199
|
+
# convention -- the 24 equally valid representations of one grain spread over
|
|
200
|
+
# 0.96 in RGB, so a map's colour depended on which variant the indexer happened
|
|
201
|
+
# to store. Two grains agreeing to 0.5 deg came out 0.42 apart in RGB.
|
|
202
|
+
# Fixed 2026-09-03; see tests::test_ipf_colour_is_symmetry_invariant.
|
|
203
|
+
d = np.einsum("nji,j->ni", g, a) # crystal dir of the axis = g^T a
|
|
143
204
|
return direction_rgb(d, space_group, gamma=gamma)
|
|
144
205
|
|
|
145
206
|
|
|
@@ -170,6 +231,8 @@ def direction_rgb(
|
|
|
170
231
|
red = red[np.arange(red.shape[0]), pick]
|
|
171
232
|
red /= np.linalg.norm(red, axis=1, keepdims=True)
|
|
172
233
|
rgb = _rgb_cubic(red)
|
|
234
|
+
elif fam == TRIGONAL:
|
|
235
|
+
rgb = _rgb_trigonal(d, sym)
|
|
173
236
|
else:
|
|
174
237
|
dd = d.copy()
|
|
175
238
|
dd[:, :, 2] = np.abs(dd[:, :, 2])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: midas-plotting
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.1
|
|
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.4.
|
|
7
|
+
version = "0.4.1"
|
|
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" }
|
|
@@ -72,7 +72,11 @@ def test_symmetry_equivalent_orientations_get_the_same_colour():
|
|
|
72
72
|
|
|
73
73
|
base = ipf_rgb(e0, 225)
|
|
74
74
|
for op in S[1:6]:
|
|
75
|
-
|
|
75
|
+
# symmetry acts on the RIGHT for crystal->lab matrices (MIDAS convention):
|
|
76
|
+
# midas_stress.misorientation_om_batch calls g.S equivalent and S.g a
|
|
77
|
+
# DIFFERENT orientation (61 deg). Asserting the wrong side is what let the
|
|
78
|
+
# g-vs-g^T bug survive until 2026-09-03.
|
|
79
|
+
g1 = np.einsum("nij,jk->nik", g0, op)
|
|
76
80
|
e1 = np.array([np.asarray(orient_mat_to_euler(m.ravel().tolist())).ravel()
|
|
77
81
|
for m in g1])
|
|
78
82
|
np.testing.assert_allclose(ipf_rgb(e1, 225), base, atol=1e-6)
|
|
@@ -98,3 +102,61 @@ def test_hexagonal_c_axis_along_z_is_red():
|
|
|
98
102
|
"""[0001] || Z is the first corner of the hexagonal triangle."""
|
|
99
103
|
rgb = ipf_rgb(np.zeros((1, 3)), 194)[0]
|
|
100
104
|
assert rgb[0] > 0.9
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# --- trigonal (Laue -3m), added for NMC811 R-3m / SG 166 -------------------
|
|
108
|
+
|
|
109
|
+
def _random_oms(n, seed=0):
|
|
110
|
+
rng = np.random.default_rng(seed)
|
|
111
|
+
q = rng.normal(size=(n, 4))
|
|
112
|
+
q /= np.linalg.norm(q, axis=1, keepdims=True)
|
|
113
|
+
w, x, y, z = q.T
|
|
114
|
+
return np.stack([
|
|
115
|
+
1 - 2 * (y * y + z * z), 2 * (x * y - z * w), 2 * (x * z + y * w),
|
|
116
|
+
2 * (x * y + z * w), 1 - 2 * (x * x + z * z), 2 * (y * z - x * w),
|
|
117
|
+
2 * (x * z - y * w), 2 * (y * z + x * w), 1 - 2 * (x * x + y * y),
|
|
118
|
+
], axis=1).reshape(-1, 3, 3)
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
@pytest.mark.parametrize("sg", [166, 194, 225])
|
|
122
|
+
def test_ipf_colour_is_symmetry_invariant(sg):
|
|
123
|
+
"""colour(S.g) == colour(g) for every proper rotation S.
|
|
124
|
+
|
|
125
|
+
This is the property that makes an IPF map mean anything: one grain, one
|
|
126
|
+
colour, whichever symmetrically equivalent orientation the indexer
|
|
127
|
+
happened to report. It is also what caught the first -3m triangle, which
|
|
128
|
+
hand-folded the azimuth assuming a 2-fold axis at 0 deg and moved the
|
|
129
|
+
colour by up to 0.99 under this test.
|
|
130
|
+
"""
|
|
131
|
+
from midas_plotting.ipf import ipf_rgb_from_matrix, sym_matrices
|
|
132
|
+
g = _random_oms(2000, seed=sg)
|
|
133
|
+
base = ipf_rgb_from_matrix(g, sg)
|
|
134
|
+
for s in sym_matrices(sg):
|
|
135
|
+
got = ipf_rgb_from_matrix(np.einsum("nij,jk->nik", g, s), sg)
|
|
136
|
+
np.testing.assert_allclose(got, base, atol=1e-9)
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def test_trigonal_sector_is_twice_the_hexagonal_one():
|
|
140
|
+
"""-3m has 6 proper rotations to 6/mmm's 12, so a 60 deg sector, not 30.
|
|
141
|
+
|
|
142
|
+
Measured from the operator set, so a change there cannot silently
|
|
143
|
+
mis-scale the ramp.
|
|
144
|
+
"""
|
|
145
|
+
from midas_plotting.ipf import _trigonal_sector_deg, sym_matrices
|
|
146
|
+
assert _trigonal_sector_deg(sym_matrices(166)) == pytest.approx(60.0)
|
|
147
|
+
assert _trigonal_sector_deg(sym_matrices(194)) == pytest.approx(30.0)
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
def test_trigonal_is_not_the_hexagonal_colouring():
|
|
151
|
+
"""R-3m must not be quietly coloured as 6/mmm -- that merges orientations."""
|
|
152
|
+
from midas_plotting.ipf import ipf_rgb_from_matrix
|
|
153
|
+
g = _random_oms(3000, seed=7)
|
|
154
|
+
assert np.abs(ipf_rgb_from_matrix(g, 166)
|
|
155
|
+
- ipf_rgb_from_matrix(g, 194)).mean() > 0.05
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def test_laue_class_still_refuses_what_it_cannot_draw():
|
|
159
|
+
from midas_plotting.ipf import laue_class
|
|
160
|
+
assert laue_class(166) == "trigonal"
|
|
161
|
+
with pytest.raises(NotImplementedError):
|
|
162
|
+
laue_class(2) # triclinic: no triangle implemented
|
|
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
|
|
File without changes
|