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.
Files changed (23) hide show
  1. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/PKG-INFO +1 -1
  2. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/__init__.py +1 -1
  3. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/ipf.py +66 -3
  4. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/PKG-INFO +1 -1
  5. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/pyproject.toml +1 -1
  6. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/tests/test_ipf.py +63 -1
  7. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/README.md +0 -0
  8. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/cli.py +0 -0
  9. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/ff.py +0 -0
  10. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/grains.py +0 -0
  11. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/laue.py +0 -0
  12. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/maps.py +0 -0
  13. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/mic.py +0 -0
  14. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting/solutions.py +0 -0
  15. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/SOURCES.txt +0 -0
  16. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/dependency_links.txt +0 -0
  17. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/entry_points.txt +0 -0
  18. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/requires.txt +0 -0
  19. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/midas_plotting.egg-info/top_level.txt +0 -0
  20. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/setup.cfg +0 -0
  21. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/tests/test_ff.py +0 -0
  22. {midas_plotting-0.4.0 → midas_plotting-0.4.1}/tests/test_laue.py +0 -0
  23. {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.0
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.0"
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
- d = np.einsum("nij,j->ni", g, a) # crystal dir of the axis
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.0
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.0"
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
- g1 = np.einsum("ij,njk->nik", op, g0)
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