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.
Files changed (23) hide show
  1. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/PKG-INFO +1 -1
  2. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/__init__.py +1 -1
  3. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/ff.py +11 -9
  4. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/ipf.py +101 -2
  5. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/PKG-INFO +1 -1
  6. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/pyproject.toml +1 -1
  7. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/tests/test_ipf.py +46 -4
  8. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/README.md +0 -0
  9. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/cli.py +0 -0
  10. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/grains.py +0 -0
  11. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/laue.py +0 -0
  12. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/maps.py +0 -0
  13. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/mic.py +0 -0
  14. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting/solutions.py +0 -0
  15. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/SOURCES.txt +0 -0
  16. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/dependency_links.txt +0 -0
  17. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/entry_points.txt +0 -0
  18. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/requires.txt +0 -0
  19. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/midas_plotting.egg-info/top_level.txt +0 -0
  20. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/setup.cfg +0 -0
  21. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/tests/test_ff.py +0 -0
  22. {midas_plotting-0.4.1 → midas_plotting-0.5.0}/tests/test_laue.py +0 -0
  23. {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.4.1
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.4.1"
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, CUBIC
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
- if fam == CUBIC:
101
- corners = np.array([[0, 0, 1.0], [1, 0, 1.0], [1, 1, 1.0]])
102
- else:
103
- corners = np.array([[0, 0, 1.0], [1, 0, 0.0], [np.sqrt(3) / 2, 0.5, 0.0]])
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) & (az <= 30.0 + 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", "sym_matrices", "CUBIC", "HEXAGONAL", "TRIGONAL"]
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). Refusing to guess."
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.4.1
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.4.1"
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
- with pytest.raises(NotImplementedError, match="not implemented"):
26
- laue_class(2) # triclinic
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(139) # tetragonal
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