midas-ddd 0.1.0__py3-none-any.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.
midas_ddd/__init__.py ADDED
@@ -0,0 +1,123 @@
1
+ """midas-ddd — dislocation networks for MIDAS.
2
+
3
+ Ingests a discrete-dislocation-dynamics network (ExaDiS / ParaDiS) and turns it
4
+ into the displacement fields that three MIDAS forward models consume:
5
+
6
+ real space beta(r) -> midas_dfxm (DFXM contrast)
7
+ Fourier u~(q) -> midas_saxs (small-angle, evaluated at q)
8
+ Fourier u~(q) -> midas_defect (near-Bragg diffuse, at G + q)
9
+
10
+ One kernel, three evaluation points. Everything is torch-differentiable.
11
+
12
+ from midas_ddd import cubic_stiffness, fcc_slip_systems
13
+
14
+ Why this package exists
15
+ -----------------------
16
+ The anisotropic-elasticity (Stroh) primitives here were previously private to
17
+ ``midas_defect.contrast_factor``, which already carried a "single source of
18
+ truth, do NOT re-port" contract because ``midas_dfxm`` imported them across a
19
+ package boundary. Two more consumers made that arrangement untenable: a SAXS
20
+ package should not have to depend on an FF-HEDM metrology package to build a
21
+ stiffness matrix. ``midas_defect.contrast_factor`` now re-exports every name,
22
+ so no historical import path broke.
23
+
24
+ ExaDiS itself requires a Kokkos/CMake build and is never a pip dependency. The
25
+ ParaDiS ``.data`` file bridge works without it; the in-process ``pyexadis`` path
26
+ is optional and imported lazily.
27
+ """
28
+
29
+ __version__ = "0.1.0"
30
+
31
+ from .elasticity import (
32
+ bcc_slip_systems,
33
+ cubic_stiffness,
34
+ fcc_slip_systems,
35
+ hexagonal_stiffness,
36
+ )
37
+ from .generate import (
38
+ combine,
39
+ polygon_area_exact_um2,
40
+ prismatic_loop,
41
+ straight_line,
42
+ )
43
+ from .fourier import (
44
+ FourierResult,
45
+ acoustic_tensor,
46
+ isotropic_stiffness,
47
+ loop_small_q_limit,
48
+ prismatic_loop_small_q_limit,
49
+ prismatic_loop_small_q_limit_total,
50
+ q_dot_u_tilde,
51
+ q_dot_u_tilde_per_loop,
52
+ small_angle_amplitude,
53
+ surface_form_factor,
54
+ u_tilde,
55
+ )
56
+ from .network import DislocationNetwork, read_paradis, write_paradis
57
+ from .realspace import (
58
+ SegmentDislocation,
59
+ green_gradient_isotropic,
60
+ lame_from_voigt,
61
+ network_distortion,
62
+ segment_dislocations,
63
+ )
64
+ from .validate import (
65
+ BurgersConservationResult,
66
+ Loop,
67
+ ResolutionReport,
68
+ check_burgers_conservation,
69
+ find_loops,
70
+ loop_area_vectors_um2,
71
+ relaxation_volumes_um3,
72
+ resolution_report,
73
+ validate_network,
74
+ )
75
+
76
+ __all__ = [
77
+ # elasticity
78
+ "bcc_slip_systems",
79
+ "cubic_stiffness",
80
+ "fcc_slip_systems",
81
+ "hexagonal_stiffness",
82
+ # network
83
+ "DislocationNetwork",
84
+ "read_paradis",
85
+ "write_paradis",
86
+ # generators (ExaDiS-free equivalents; see midas_ddd.exadis for the real ones)
87
+ "combine",
88
+ "polygon_area_exact_um2",
89
+ "prismatic_loop",
90
+ "straight_line",
91
+ # Fourier kernel
92
+ "FourierResult",
93
+ "acoustic_tensor",
94
+ "isotropic_stiffness",
95
+ "loop_small_q_limit",
96
+ "prismatic_loop_small_q_limit",
97
+ "prismatic_loop_small_q_limit_total",
98
+ "q_dot_u_tilde",
99
+ "q_dot_u_tilde_per_loop",
100
+ "small_angle_amplitude",
101
+ "surface_form_factor",
102
+ "u_tilde",
103
+ # real-space distortion (ELASTIC part; see the module docstring)
104
+ "SegmentDislocation",
105
+ "green_gradient_isotropic",
106
+ "lame_from_voigt",
107
+ "network_distortion",
108
+ "segment_dislocations",
109
+ # validation
110
+ "BurgersConservationResult",
111
+ "Loop",
112
+ "ResolutionReport",
113
+ "check_burgers_conservation",
114
+ "find_loops",
115
+ "loop_area_vectors_um2",
116
+ "relaxation_volumes_um3",
117
+ "resolution_report",
118
+ "validate_network",
119
+ "__version__",
120
+ ]
121
+
122
+ # `midas_ddd.exadis` is NOT imported here: it pulls pyexadis, which needs a
123
+ # Kokkos build. Import it explicitly when you have one.
midas_ddd/_g5.py ADDED
@@ -0,0 +1 @@
1
+ DATASET = "smith_cu_sep24"
@@ -0,0 +1,294 @@
1
+ """Anisotropic-elasticity primitives for dislocations (Stroh sextic solution).
2
+
3
+ **Single source of truth.** These primitives used to live in
4
+ ``midas_defect.contrast_factor`` / ``midas_defect.contrast_factor_hex``, which
5
+ already carried a "do NOT re-port" contract because ``midas_dfxm.dislocation``
6
+ imports them. They moved down here when a third and fourth consumer appeared
7
+ (``midas_saxs`` and the near-Bragg diffuse forward), so that dislocation
8
+ elasticity has exactly one home and nothing has to depend on an FF-HEDM
9
+ metrology package to get a stiffness matrix. ``midas_defect.contrast_factor``
10
+ re-exports every name below, so every historical import path still works.
11
+
12
+ What is here
13
+ ------------
14
+ * Stiffness construction: :func:`cubic_stiffness`, :func:`hexagonal_stiffness`.
15
+ * Voigt ↔ 4-tensor and frame rotation: :func:`_voigt_to_tensor`,
16
+ :func:`_rotate_tensor`, :func:`_slip_frame`.
17
+ * The Stroh sextic eigensolution :func:`_stroh_eig` — 3 roots ``p`` with
18
+ ``Im(p) > 0`` plus the amplitude/stress matrices ``A``, ``B``.
19
+ * Crystal → Cartesian geometry for non-cubic cells: :func:`_crystal_A_matrix`,
20
+ :func:`_to_cartesian`.
21
+ * Slip-system tables: :func:`fcc_slip_systems`, :func:`bcc_slip_systems`.
22
+
23
+ The physics reference is unchanged: Teodosiu 1982 / Ting, *Anisotropic
24
+ Elasticity*, as implemented by ANIZC (Borbély, Dragomir-Cernatescu, Ribárik &
25
+ Ungár, *J. Appl. Cryst.* **36** (2003) 160-162).
26
+
27
+ Conventions
28
+ -----------
29
+ * Elastic constants: full 6×6 Voigt stiffness matrix, any consistent unit.
30
+ * Slip coordinate system: ``e3`` = dislocation line, ``e2`` = slip-plane normal,
31
+ ``e1 = e2 × e3``. Character ψ = ∠(line, Burgers): 0° = screw, 90° = edge.
32
+ * Burgers vector, slip-plane normal, line and diffraction ``g`` in crystal
33
+ Cartesian axes; for a cubic cell these coincide with the integer ``(hkl)`` /
34
+ ``[uvw]`` axes.
35
+
36
+ Differentiability / device
37
+ --------------------------
38
+ Pure torch: the Voigt→4-tensor expansion, the slip-frame rotation and the 6×6
39
+ Stroh eigenproblem (:func:`torch.linalg.eig`) are all differentiable w.r.t. the
40
+ elastic constants. ``complex128`` eig is unsupported on the MPS backend, so this
41
+ path runs on CPU / CUDA.
42
+ """
43
+
44
+ from __future__ import annotations
45
+
46
+ from typing import List, Sequence, Tuple
47
+
48
+ import numpy as np
49
+ import torch
50
+
51
+ __all__ = [
52
+ "cubic_stiffness",
53
+ "hexagonal_stiffness",
54
+ "fcc_slip_systems",
55
+ "bcc_slip_systems",
56
+ ]
57
+
58
+
59
+ # ---------------------------------------------------------------------------
60
+ # Voigt index map and stiffness construction
61
+ # ---------------------------------------------------------------------------
62
+
63
+ #: Voigt index for each (i, j) Cartesian pair: 11→0 22→1 33→2 23→3 13→4 12→5.
64
+ _VOIGT_IDX = torch.tensor([[0, 5, 4], [5, 1, 3], [4, 3, 2]])
65
+
66
+
67
+ def cubic_stiffness(
68
+ c11: float, c12: float, c44: float, *,
69
+ dtype: torch.dtype = torch.float64,
70
+ device=None,
71
+ ) -> torch.Tensor:
72
+ """Return the 6×6 Voigt stiffness matrix of a cubic crystal.
73
+
74
+ Parameters
75
+ ----------
76
+ c11, c12, c44
77
+ Cubic elastic constants (GPa, or any consistent unit). ``C`` is
78
+ dimensionless so only their ratios matter.
79
+ """
80
+ device = torch.device("cpu") if device is None else device
81
+ c11 = torch.as_tensor(c11, dtype=dtype, device=device)
82
+ c12 = torch.as_tensor(c12, dtype=dtype, device=device)
83
+ c44 = torch.as_tensor(c44, dtype=dtype, device=device)
84
+ z = torch.zeros((), dtype=dtype, device=device)
85
+ rows = [
86
+ torch.stack([c11, c12, c12, z, z, z]),
87
+ torch.stack([c12, c11, c12, z, z, z]),
88
+ torch.stack([c12, c12, c11, z, z, z]),
89
+ torch.stack([z, z, z, c44, z, z]),
90
+ torch.stack([z, z, z, z, c44, z]),
91
+ torch.stack([z, z, z, z, z, c44]),
92
+ ]
93
+ return torch.stack(rows, dim=0)
94
+
95
+
96
+ def _voigt_to_tensor(C6: torch.Tensor) -> torch.Tensor:
97
+ """Expand a 6×6 Voigt stiffness matrix to the full ``(3,3,3,3)`` tensor."""
98
+ idx = _VOIGT_IDX.to(C6.device)
99
+ return C6[idx[:, :, None, None], idx[None, None, :, :]]
100
+
101
+
102
+ def _rotate_tensor(C4: torch.Tensor, M: torch.Tensor) -> torch.Tensor:
103
+ """Rotate a 4th-rank tensor: ``C'_{ijkl} = M_ia M_jb M_kc M_ld C_{abcd}``."""
104
+ return torch.einsum("ia,jb,kc,ld,abcd->ijkl", M, M, M, M, C4)
105
+
106
+
107
+ def _slip_frame(line: torch.Tensor, normal: torch.Tensor) -> torch.Tensor:
108
+ """Rotation matrix (rows = e1, e2, e3) from crystal to slip coordinates.
109
+
110
+ ``e3`` = unit line, ``e2`` = unit slip-plane normal, ``e1 = e2 × e3``. The
111
+ returned ``M`` maps a crystal-frame vector ``v`` to the slip frame via
112
+ ``M @ v``. ``normal`` is expected ⊥ ``line`` (a physical slip system); a
113
+ Gram-Schmidt step removes any residual non-orthogonality.
114
+ """
115
+ e3 = line / torch.linalg.norm(line)
116
+ e2 = normal - (normal @ e3) * e3
117
+ e2 = e2 / torch.linalg.norm(e2)
118
+ e1 = torch.linalg.cross(e2, e3)
119
+ e1 = e1 / torch.linalg.norm(e1)
120
+ return torch.stack([e1, e2, e3], dim=0)
121
+
122
+
123
+ def _stroh_eig(
124
+ C4: torch.Tensor,
125
+ *,
126
+ degeneracy_tol: float = 1e-4,
127
+ ) -> Tuple[torch.Tensor, torch.Tensor, torch.Tensor]:
128
+ """Stroh eigensolution: 3 roots ``p`` (Im>0) and amplitude/stress vectors A,B.
129
+
130
+ Solves the 6×6 fundamental eigenproblem ``N ξ = p ξ`` with
131
+ ``ξ = (a, b)``, where for a dislocation line along ``e3``::
132
+
133
+ Q_ik = C_{i1k1}, R_ik = C_{i1k2}, T_ik = C_{i2k2}
134
+ N = [[ -T⁻¹Rᵀ , T⁻¹ ],
135
+ [ R T⁻¹Rᵀ - Q , -R T⁻¹ ]]
136
+
137
+ Returns ``(p, A, B)`` for the three roots with positive imaginary part,
138
+ with columns normalised so ``2 aₐᵀ bₐ = 1`` (Stroh orthonormality).
139
+ """
140
+ Q = C4[:, 0, :, 0]
141
+ R = C4[:, 0, :, 1]
142
+ T = C4[:, 1, :, 1]
143
+ Tinv = torch.linalg.inv(T)
144
+ RT = R.transpose(-1, -2)
145
+ N1 = -Tinv @ RT
146
+ N3 = R @ Tinv @ RT - Q
147
+ top = torch.cat([N1, Tinv], dim=1)
148
+ bot = torch.cat([N3, N1.transpose(-1, -2)], dim=1)
149
+ N = torch.cat([top, bot], dim=0)
150
+
151
+ p_all, _ = torch.linalg.eig(N)
152
+ mask = p_all.imag > 0
153
+ if int(mask.sum()) != 3:
154
+ raise ValueError(
155
+ f"expected 3 sextic roots with Im(p)>0, got {int(mask.sum())}; "
156
+ "the stiffness may be elastically isotropic (degenerate roots) or "
157
+ "ill-conditioned."
158
+ )
159
+ p = p_all[mask]
160
+
161
+ # Near-degenerate roots ⇒ the matrix is defective and the simple-eigenvector
162
+ # Stroh path returns unreliable amplitude vectors. This happens in the
163
+ # elastically isotropic limit (Zener ratio → 1), where the roots collapse to
164
+ # p = i. Refuse rather than silently return garbage — a dislocation-contrast
165
+ # analysis is only meaningful for an elastically anisotropic crystal anyway.
166
+ pdiff = torch.abs(p[:, None] - p[None, :])
167
+ pdiff = pdiff + torch.eye(3, dtype=pdiff.dtype, device=pdiff.device) * 9.0
168
+ if float(pdiff.min().detach()) < degeneracy_tol:
169
+ raise ValueError(
170
+ "near-degenerate sextic roots: the crystal is at/near the elastically "
171
+ "isotropic limit (Zener ratio ≈ 1), where the simple-eigenvector Stroh "
172
+ "solution is unreliable. Contrast factors require an anisotropic crystal."
173
+ )
174
+
175
+ # Amplitude vectors a from the 3×3 null space of (Q + p(R+Rᵀ) + p²T):
176
+ Qc, Rc, Tc, RTc = (Q.to(p.dtype), R.to(p.dtype),
177
+ T.to(p.dtype), RT.to(p.dtype))
178
+ eye3 = torch.eye(3, dtype=p.dtype, device=C4.device)
179
+ a_cols, b_cols = [], []
180
+ for k in range(3):
181
+ Mk = Qc + p[k] * (Rc + RTc) + p[k] * p[k] * Tc
182
+ # null vector = singular vector with smallest singular value
183
+ _, _, Vh = torch.linalg.svd(Mk)
184
+ a = Vh[-1].conj()
185
+ b = (RTc + p[k] * Tc) @ a # Stroh stress vector
186
+ s = 1.0 / torch.sqrt(2.0 * (a @ b)) # bilinear (no conjugate)
187
+ a_cols.append(a * s)
188
+ b_cols.append(b * s)
189
+ A = torch.stack(a_cols, dim=1)
190
+ B = torch.stack(b_cols, dim=1)
191
+ return p, A, B
192
+
193
+
194
+ # ---------------------------------------------------------------------------
195
+ # Hexagonal stiffness (moved from midas_defect.contrast_factor_hex)
196
+ # ---------------------------------------------------------------------------
197
+
198
+ def hexagonal_stiffness(
199
+ c11: float, c12: float, c13: float, c33: float, c44: float, *,
200
+ dtype: torch.dtype = torch.float64, device=None,
201
+ ) -> torch.Tensor:
202
+ """6×6 Voigt stiffness of a hexagonal crystal (``C66 = (C11-C12)/2``)."""
203
+ device = torch.device("cpu") if device is None else device
204
+ t = lambda x: torch.as_tensor(x, dtype=dtype, device=device)
205
+ c11, c12, c13, c33, c44 = t(c11), t(c12), t(c13), t(c33), t(c44)
206
+ c66 = (c11 - c12) / 2.0
207
+ z = torch.zeros((), dtype=dtype, device=device)
208
+ rows = [
209
+ torch.stack([c11, c12, c13, z, z, z]),
210
+ torch.stack([c12, c11, c13, z, z, z]),
211
+ torch.stack([c13, c13, c33, z, z, z]),
212
+ torch.stack([z, z, z, c44, z, z]),
213
+ torch.stack([z, z, z, z, c44, z]),
214
+ torch.stack([z, z, z, z, z, c66]),
215
+ ]
216
+ return torch.stack(rows, dim=0)
217
+
218
+
219
+ # ---------------------------------------------------------------------------
220
+ # Crystal → Cartesian geometry (any crystal system)
221
+ # ---------------------------------------------------------------------------
222
+ #
223
+ # The Stroh core works in a Cartesian slip frame. For a *cubic* cell the integer
224
+ # Miller indices double as Cartesian directions, but for any lower symmetry they
225
+ # do not. We reuse the canonical orthogonalisation ("A") matrix from
226
+ # `midas_stress.tensor.lattice_params_to_A_matrix` (Busing-Levy convention, the
227
+ # same matrix the Fable-Beaudoin strain solver uses) — never a re-ported B
228
+ # matrix. ``A`` maps fractional crystal coordinates to Cartesian, so:
229
+ # * a real-space direction [uvw] (Burgers vector, dislocation line) → A · [uvw]
230
+ # * a reciprocal direction (hkl) (plane normal, diffraction vector) → A⁻ᵀ · (hkl)
231
+ # For cubic, A = a·I, so both maps are the identity up to scale and every result
232
+ # is byte-identical to passing raw Miller indices (see the cubic-identity test).
233
+
234
+
235
+ def _crystal_A_matrix(crystal, *, dtype, device) -> torch.Tensor:
236
+ """Orthogonalisation matrix A (fractional → Cartesian) for a `Crystal`."""
237
+ from midas_stress.tensor import lattice_params_to_A_matrix
238
+
239
+ lat = crystal.lattice
240
+ latc = torch.tensor(
241
+ [lat.a, lat.b, lat.c, lat.alpha, lat.beta, lat.gamma],
242
+ dtype=dtype, device=device)
243
+ return lattice_params_to_A_matrix(latc)
244
+
245
+
246
+ def _to_cartesian(v: torch.Tensor, A: torch.Tensor, kind: str) -> torch.Tensor:
247
+ """Map a Miller vector to Cartesian: ``direct`` → A·v, ``reciprocal`` → A⁻ᵀ·v."""
248
+ if kind == "direct":
249
+ return A @ v
250
+ if kind == "reciprocal":
251
+ return torch.linalg.solve(A.transpose(-1, -2), v) # A⁻ᵀ · v
252
+ raise ValueError(f"kind must be 'direct' or 'reciprocal', got {kind!r}")
253
+
254
+
255
+ # ---------------------------------------------------------------------------
256
+ # Slip-system tables
257
+ # ---------------------------------------------------------------------------
258
+
259
+ def _gen_slip_systems(
260
+ planes: Sequence[Tuple[int, int, int]],
261
+ burgers_pool: Sequence[Tuple[int, int, int]],
262
+ ) -> List[Tuple[Tuple[int, int, int], Tuple[int, int, int]]]:
263
+ """All (normal, burgers) pairs with burgers ⊥ normal, ± deduplicated."""
264
+ systems = []
265
+ seen = set()
266
+ for n in planes:
267
+ nv = np.array(n, float)
268
+ for b in burgers_pool:
269
+ bv = np.array(b, float)
270
+ if abs(float(nv @ bv)) > 1e-9:
271
+ continue
272
+ key = (n, tuple(sorted((b, tuple(-x for x in b)))))
273
+ if key in seen:
274
+ continue
275
+ seen.add(key)
276
+ systems.append((n, b))
277
+ return systems
278
+
279
+
280
+ def fcc_slip_systems() -> List[Tuple[Tuple[int, int, int], Tuple[int, int, int]]]:
281
+ """The 12 FCC ``{111}⟨110⟩`` slip systems as ``(plane_normal, burgers)``."""
282
+ planes = [(1, 1, 1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)] # 4 distinct {111}
283
+ burgers = [(1, -1, 0), (1, 0, -1), (0, 1, -1),
284
+ (1, 1, 0), (1, 0, 1), (0, 1, 1)]
285
+ return _gen_slip_systems(planes, burgers)
286
+
287
+
288
+ def bcc_slip_systems() -> List[Tuple[Tuple[int, int, int], Tuple[int, int, int]]]:
289
+ """The 12 BCC ``{110}⟨111⟩`` slip systems as ``(plane_normal, burgers)``."""
290
+ planes = [(1, 1, 0), (1, -1, 0), (1, 0, 1), # 6 distinct {110}
291
+ (1, 0, -1), (0, 1, 1), (0, 1, -1)]
292
+ burgers = [(1, 1, 1), (1, 1, -1), (1, -1, 1), (-1, 1, 1)]
293
+ return _gen_slip_systems(planes, burgers)
294
+