midas-diffract 0.2.0__tar.gz → 0.4.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 (25) hide show
  1. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/PKG-INFO +2 -1
  2. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract/__init__.py +1 -1
  3. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract/forward.py +69 -41
  4. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract.egg-info/PKG-INFO +2 -1
  5. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract.egg-info/requires.txt +1 -0
  6. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/pyproject.toml +2 -1
  7. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/tests/test_forward.py +90 -0
  8. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/LICENSE +0 -0
  9. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/README.md +0 -0
  10. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract/hkls.py +0 -0
  11. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract/losses.py +0 -0
  12. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract/optimize.py +0 -0
  13. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract/simulate_panel_zarrs.py +0 -0
  14. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract.egg-info/SOURCES.txt +0 -0
  15. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract.egg-info/dependency_links.txt +0 -0
  16. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/midas_diffract.egg-info/top_level.txt +0 -0
  17. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/setup.cfg +0 -0
  18. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/tests/test_c_comparison.py +0 -0
  19. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/tests/test_distortion_layer.py +0 -0
  20. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/tests/test_hkls.py +0 -0
  21. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/tests/test_losses.py +0 -0
  22. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/tests/test_multi_detector.py +0 -0
  23. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/tests/test_strain_tensor.py +0 -0
  24. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/tests/test_tilts.py +0 -0
  25. {midas_diffract-0.2.0 → midas_diffract-0.4.0}/tests/test_wedge.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: midas-diffract
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: End-to-end differentiable forward model for High-Energy Diffraction Microscopy (FF, NF, pf-HEDM)
5
5
  Author-email: Hemant Sharma <hsharma@anl.gov>
6
6
  License-Expression: BSD-3-Clause
@@ -19,6 +19,7 @@ License-File: LICENSE
19
19
  Requires-Dist: numpy>=1.22
20
20
  Requires-Dist: torch>=2.0
21
21
  Requires-Dist: midas-distortion>=0.2.0
22
+ Requires-Dist: midas-stress>=0.8.0
22
23
  Provides-Extra: dev
23
24
  Requires-Dist: pytest>=7.0; extra == "dev"
24
25
  Requires-Dist: pytest-cov; extra == "dev"
@@ -28,7 +28,7 @@ Quick start
28
28
  loss.backward()
29
29
  """
30
30
 
31
- __version__ = "0.2.0"
31
+ __version__ = "0.4.0"
32
32
 
33
33
  from .forward import (
34
34
  HEDMForwardModel,
@@ -34,6 +34,16 @@ import torch
34
34
  import torch.nn as nn
35
35
  import torch.nn.functional as F
36
36
 
37
+ # Canonical orientation + strain-frame primitives. midas_stress is the single
38
+ # source of truth for this math (Bunge ZXZ orientation algebra, sample<->crystal
39
+ # strain rotation); its torch backend is differentiable end-to-end and
40
+ # device-portable, so the forward model delegates rather than re-porting.
41
+ # NOTE: midas_stress's Voigt is Voigt-MANDEL (sqrt2 on shears); this model uses
42
+ # PLAIN-Voigt / raw 3x3 strain, so we only delegate the *rotation*, never the
43
+ # Voigt packing (see rotate_strain_sample_to_crystal / correct_hkls_latc).
44
+ from midas_stress.orientation import euler_to_orient_mat as _ms_euler_to_orient_mat
45
+ from midas_stress.tensor import strain_lab_to_grain as _ms_strain_lab_to_grain
46
+
37
47
 
38
48
  # ---------------------------------------------------------------------------
39
49
  # Configuration data classes
@@ -437,7 +447,13 @@ class HEDMForwardModel(nn.Module):
437
447
 
438
448
  @staticmethod
439
449
  def euler2mat(euler_angles: torch.Tensor) -> torch.Tensor:
440
- """Convert ZXZ Euler angles to rotation matrices.
450
+ """Convert ZXZ (Bunge) Euler angles to crystal->sample rotation matrices.
451
+
452
+ Delegates to ``midas_stress.orientation.euler_to_orient_mat`` -- the
453
+ canonical orientation primitive -- so the convention can never drift
454
+ from the rest of MIDAS. midas_stress's torch backend is differentiable
455
+ and vmap-safe; the result is identical to the former in-line ZXZ build
456
+ (R = Rz(phi1) @ Rx(Phi) @ Rz(phi2)) to ~1e-16.
441
457
 
442
458
  Parameters
443
459
  ----------
@@ -447,35 +463,14 @@ class HEDMForwardModel(nn.Module):
447
463
  Returns
448
464
  -------
449
465
  Tensor (..., 3, 3)
450
- Rotation matrices.
466
+ Rotation matrices (crystal->sample), orthogonalized onto SO(3).
451
467
  """
452
- c = torch.cos(euler_angles)
453
- s = torch.sin(euler_angles)
454
-
455
- c0, c1, c2 = c[..., 0], c[..., 1], c[..., 2]
456
- s0, s1, s2 = s[..., 0], s[..., 1], s[..., 2]
457
-
458
- # ZXZ rotation matrix: R = Rz(phi1) @ Rx(Phi) @ Rz(phi2)
459
- # Verified element-by-element against nfhedm.py lines 114-120.
460
- # Built via torch.stack rather than indexed assignment so the function
461
- # composes with torch.func.vmap (in-place writes block vmap).
462
- row0 = torch.stack([
463
- c0 * c2 - s0 * c1 * s2,
464
- -s0 * c1 * c2 - c0 * s2,
465
- s0 * s1,
466
- ], dim=-1)
467
- row1 = torch.stack([
468
- s0 * c2 + c0 * c1 * s2,
469
- c0 * c1 * c2 - s0 * s2,
470
- -c0 * s1,
471
- ], dim=-1)
472
- row2 = torch.stack([
473
- s1 * s2,
474
- s1 * c2,
475
- c1,
476
- ], dim=-1)
477
- R = torch.stack([row0, row1, row2], dim=-2)
478
-
468
+ if not isinstance(euler_angles, torch.Tensor):
469
+ euler_angles = torch.as_tensor(euler_angles)
470
+ R = _ms_euler_to_orient_mat(euler_angles) # (..., 9), torch
471
+ R = R.reshape(*R.shape[:-1], 3, 3)
472
+ # midas_stress already returns a proper rotation; orthogonalize keeps the
473
+ # historical "exactly on SO(3)" guarantee and is idempotent here.
479
474
  return HEDMForwardModel.orthogonalize(R)
480
475
 
481
476
  # ------------------------------------------------------------------
@@ -525,6 +520,28 @@ class HEDMForwardModel(nn.Module):
525
520
  """Numerically stable arccos: clamp to [-1+eps, 1-eps]."""
526
521
  return torch.acos(torch.clamp(x, -1.0 + self.epsilon, 1.0 - self.epsilon))
527
522
 
523
+ # ------------------------------------------------------------------
524
+ # strain_as_voigt (accept full 3x3 tensor OR plain-Voigt 6-vector)
525
+ # ------------------------------------------------------------------
526
+
527
+ @staticmethod
528
+ def strain_as_voigt(strain: torch.Tensor) -> torch.Tensor:
529
+ """Normalize a strain input to PLAIN-Voigt [e11,e12,e13,e22,e23,e33].
530
+
531
+ Accepts either a plain-Voigt ``(..., 6)`` tensor (returned unchanged) or
532
+ a full symmetric ``(..., 3, 3)`` strain tensor. The 3x3 path is
533
+ convention-free -- the natural way to hand a strain field straight from
534
+ a tensor source (e.g. a midas_stress strain field) into the forward
535
+ model without picking a Voigt/Mandel packing. The off-diagonals are
536
+ taken as TRUE tensor components (no factor of 2).
537
+ """
538
+ if strain.dim() >= 2 and strain.shape[-1] == 3 and strain.shape[-2] == 3:
539
+ return torch.stack([
540
+ strain[..., 0, 0], strain[..., 0, 1], strain[..., 0, 2],
541
+ strain[..., 1, 1], strain[..., 1, 2], strain[..., 2, 2],
542
+ ], dim=-1)
543
+ return strain
544
+
528
545
  # ------------------------------------------------------------------
529
546
  # rotate_strain_sample_to_crystal (port of C RotateStrainSampleToCrystal)
530
547
  # ------------------------------------------------------------------
@@ -536,19 +553,27 @@ class HEDMForwardModel(nn.Module):
536
553
  ) -> torch.Tensor:
537
554
  """Rotate a symmetric infinitesimal strain from sample to crystal frame.
538
555
 
539
- Port of ``RotateStrainSampleToCrystal`` from
556
+ Matches ``RotateStrainSampleToCrystal`` from
540
557
  ``FF_HEDM/src/ForwardSimulationCompressed.c:399-419``:
541
- eps_crystal = OM^T . eps_sample . OM, in Voigt notation
542
- [eps_11, eps_12, eps_13, eps_22, eps_23, eps_33].
558
+ eps_crystal = OM^T . eps_sample . OM.
559
+
560
+ The rotation is delegated to ``midas_stress.tensor.strain_lab_to_grain``
561
+ (the canonical sample/lab -> crystal/grain strain transform; bit-identical
562
+ to the former in-line ``OM^T S OM``). PLAIN-Voigt pack/unpack is kept here
563
+ on purpose -- midas_stress's Voigt is Mandel (sqrt2 on shears), which must
564
+ not touch the forward model's strain input.
543
565
 
544
566
  Parameters
545
567
  ----------
546
568
  orientation_matrices : Tensor (..., 3, 3)
569
+ Crystal->sample matrices (as returned by :meth:`euler2mat`).
547
570
  strain_sample : Tensor (..., 6)
571
+ PLAIN-Voigt [eps_11, eps_12, eps_13, eps_22, eps_23, eps_33].
548
572
 
549
573
  Returns
550
574
  -------
551
575
  strain_crystal : Tensor (..., 6)
576
+ PLAIN-Voigt, same layout.
552
577
  """
553
578
  e = strain_sample
554
579
  S = torch.stack([
@@ -556,8 +581,7 @@ class HEDMForwardModel(nn.Module):
556
581
  torch.stack([e[..., 1], e[..., 3], e[..., 4]], dim=-1),
557
582
  torch.stack([e[..., 2], e[..., 4], e[..., 5]], dim=-1),
558
583
  ], dim=-2)
559
- OM = orientation_matrices
560
- C = torch.matmul(torch.matmul(OM.transpose(-1, -2), S), OM)
584
+ C = _ms_strain_lab_to_grain(S, orientation_matrices) # OM^T S OM
561
585
  return torch.stack([
562
586
  C[..., 0, 0], C[..., 0, 1], C[..., 0, 2],
563
587
  C[..., 1, 1], C[..., 1, 2], C[..., 2, 2],
@@ -587,10 +611,12 @@ class HEDMForwardModel(nn.Module):
587
611
  lattice_params : Tensor (..., 6)
588
612
  [a, b, c, alpha, beta, gamma] in Angstroms and degrees.
589
613
  The ``...`` dimensions allow per-voxel or per-grain parameters.
590
- strain : Tensor (..., 6), optional
591
- Crystal-frame symmetric infinitesimal strain in Voigt form
592
- [eps_11, eps_12, eps_13, eps_22, eps_23, eps_33]. When supplied,
593
- the reciprocal lattice is post-multiplied by (I + eps)^{-1}:
614
+ strain : Tensor (..., 6) or (..., 3, 3), optional
615
+ Crystal-frame symmetric infinitesimal strain, either PLAIN-Voigt
616
+ [eps_11, eps_12, eps_13, eps_22, eps_23, eps_33] or a full symmetric
617
+ 3x3 tensor (normalized via :meth:`strain_as_voigt`; off-diagonals are
618
+ true tensor components, no factor of 2). When supplied, the
619
+ reciprocal lattice is post-multiplied by (I + eps)^{-1}:
594
620
  B = (I + eps)^{-1} @ B0. Use :meth:`rotate_strain_sample_to_crystal`
595
621
  to convert a sample-frame strain into the crystal frame.
596
622
 
@@ -671,6 +697,7 @@ class HEDMForwardModel(nn.Module):
671
697
  # Voigt layout matches C CorrectHKLsLatCEpsilon:
672
698
  # eps = [eps_11, eps_12, eps_13, eps_22, eps_23, eps_33]
673
699
  if strain is not None:
700
+ strain = self.strain_as_voigt(strain) # accept full 3x3 too
674
701
  e11 = strain[..., 0]
675
702
  e12 = strain[..., 1]
676
703
  e13 = strain[..., 2]
@@ -1311,9 +1338,10 @@ class HEDMForwardModel(nn.Module):
1311
1338
  lattice_params : Tensor (..., 6) or (..., N, 6), optional
1312
1339
  Strained lattice parameters [a,b,c,alpha,beta,gamma] in
1313
1340
  Angstroms/degrees. None = use nominal hkls/thetas (no strain).
1314
- strain : Tensor (..., 6) or (..., N, 6), optional
1315
- Crystal-frame symmetric infinitesimal strain in Voigt form
1316
- [eps_11, eps_12, eps_13, eps_22, eps_23, eps_33]. Applied as
1341
+ strain : Tensor (..., 6), (..., N, 6), or (..., 3, 3), optional
1342
+ Crystal-frame symmetric infinitesimal strain, either PLAIN-Voigt
1343
+ [eps_11, eps_12, eps_13, eps_22, eps_23, eps_33] or a full symmetric
1344
+ 3x3 tensor (see :meth:`strain_as_voigt`). Applied as
1317
1345
  B = (I + eps)^{-1} @ B0 in addition to any lattice-parameter
1318
1346
  strain expressed through ``lattice_params``. Requires
1319
1347
  ``lattice_params`` to be supplied.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: midas-diffract
3
- Version: 0.2.0
3
+ Version: 0.4.0
4
4
  Summary: End-to-end differentiable forward model for High-Energy Diffraction Microscopy (FF, NF, pf-HEDM)
5
5
  Author-email: Hemant Sharma <hsharma@anl.gov>
6
6
  License-Expression: BSD-3-Clause
@@ -19,6 +19,7 @@ License-File: LICENSE
19
19
  Requires-Dist: numpy>=1.22
20
20
  Requires-Dist: torch>=2.0
21
21
  Requires-Dist: midas-distortion>=0.2.0
22
+ Requires-Dist: midas-stress>=0.8.0
22
23
  Provides-Extra: dev
23
24
  Requires-Dist: pytest>=7.0; extra == "dev"
24
25
  Requires-Dist: pytest-cov; extra == "dev"
@@ -1,6 +1,7 @@
1
1
  numpy>=1.22
2
2
  torch>=2.0
3
3
  midas-distortion>=0.2.0
4
+ midas-stress>=0.8.0
4
5
 
5
6
  [dev]
6
7
  pytest>=7.0
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "midas-diffract"
7
- version = "0.2.0"
7
+ version = "0.4.0"
8
8
  description = "End-to-end differentiable forward model for High-Energy Diffraction Microscopy (FF, NF, pf-HEDM)"
9
9
  readme = "README.md"
10
10
  license = "BSD-3-Clause"
@@ -27,6 +27,7 @@ dependencies = [
27
27
  "numpy>=1.22",
28
28
  "torch>=2.0",
29
29
  "midas-distortion>=0.2.0",
30
+ "midas-stress>=0.8.0",
30
31
  ]
31
32
 
32
33
  [project.optional-dependencies]
@@ -1111,5 +1111,95 @@ class TestCrossValidation:
1111
1111
  import torch.nn.functional as F # for test_2d_position_backward_compat
1112
1112
 
1113
1113
 
1114
+ # ===================================================================
1115
+ # Test: midas_stress is the canonical orientation/strain-frame source
1116
+ # ===================================================================
1117
+
1118
+ class TestMidasStressDelegation:
1119
+ """Guards that euler2mat / rotate_strain_sample_to_crystal delegate to
1120
+ midas_stress and never silently re-port (convention drift)."""
1121
+
1122
+ def test_euler2mat_matches_midas_stress(self):
1123
+ from midas_stress.orientation import euler_to_orient_mat
1124
+ torch.manual_seed(0)
1125
+ eul = torch.rand(7, 3, dtype=torch.float64) * 2 * math.pi
1126
+ R = HEDMForwardModel.euler2mat(eul)
1127
+ R_ms = euler_to_orient_mat(eul).reshape(7, 3, 3)
1128
+ torch.testing.assert_close(R, R_ms, atol=1e-12, rtol=0)
1129
+
1130
+ def test_euler2mat_vmap_safe(self):
1131
+ from torch.func import vmap
1132
+ eul = torch.rand(5, 3, dtype=torch.float64) * 2 * math.pi
1133
+ R = vmap(HEDMForwardModel.euler2mat)(eul)
1134
+ assert R.shape == (5, 3, 3)
1135
+ torch.testing.assert_close(R, HEDMForwardModel.euler2mat(eul), atol=1e-12, rtol=0)
1136
+
1137
+ def test_euler2mat_differentiable(self):
1138
+ eul = torch.rand(3, 3, dtype=torch.float64, requires_grad=True)
1139
+ HEDMForwardModel.euler2mat(eul).pow(2).sum().backward()
1140
+ assert eul.grad is not None and torch.all(torch.isfinite(eul.grad))
1141
+
1142
+ def test_rotate_strain_matches_midas_stress(self):
1143
+ from midas_stress.tensor import strain_lab_to_grain
1144
+ torch.manual_seed(1)
1145
+ OM = HEDMForwardModel.euler2mat(torch.rand(4, 3, dtype=torch.float64))
1146
+ v = torch.rand(4, 6, dtype=torch.float64) * 1e-3 # plain Voigt
1147
+ out = HEDMForwardModel.rotate_strain_sample_to_crystal(OM, v)
1148
+ # reference: build 3x3, rotate via midas_stress, repack plain Voigt
1149
+ S = torch.zeros(4, 3, 3, dtype=torch.float64)
1150
+ S[:, 0, 0], S[:, 1, 1], S[:, 2, 2] = v[:, 0], v[:, 3], v[:, 5]
1151
+ S[:, 0, 1] = S[:, 1, 0] = v[:, 1]
1152
+ S[:, 0, 2] = S[:, 2, 0] = v[:, 2]
1153
+ S[:, 1, 2] = S[:, 2, 1] = v[:, 4]
1154
+ C = strain_lab_to_grain(S, OM)
1155
+ ref = torch.stack([C[:, 0, 0], C[:, 0, 1], C[:, 0, 2],
1156
+ C[:, 1, 1], C[:, 1, 2], C[:, 2, 2]], dim=-1)
1157
+ torch.testing.assert_close(out, ref, atol=1e-14, rtol=0)
1158
+
1159
+
1160
+ # ===================================================================
1161
+ # Test: full 3x3 strain tensor entry point (convention-free)
1162
+ # ===================================================================
1163
+
1164
+ class TestStrainTensorInput:
1165
+ """A full symmetric 3x3 strain must give the same result as the
1166
+ equivalent plain-Voigt 6-vector (NOT Voigt-Mandel)."""
1167
+
1168
+ def test_strain_as_voigt_roundtrip(self):
1169
+ v = torch.tensor([1e-3, 2e-4, 3e-4, -5e-4, 1e-4, 2e-4])
1170
+ S = torch.tensor([[1e-3, 2e-4, 3e-4],
1171
+ [2e-4, -5e-4, 1e-4],
1172
+ [3e-4, 1e-4, 2e-4]])
1173
+ torch.testing.assert_close(HEDMForwardModel.strain_as_voigt(S), v)
1174
+ # plain Voigt passes through unchanged
1175
+ torch.testing.assert_close(HEDMForwardModel.strain_as_voigt(v), v)
1176
+
1177
+ def test_3x3_strain_equals_voigt_in_correct_hkls_latc(self, nf_geometry, device):
1178
+ model, _, _ = make_model_with_cubic_iron(nf_geometry, device)
1179
+ latc = torch.tensor([2.87, 2.87, 2.87, 90., 90., 90.])
1180
+ v = torch.tensor([1e-3, 2e-4, 3e-4, -5e-4, 1e-4, 2e-4])
1181
+ S = torch.tensor([[1e-3, 2e-4, 3e-4],
1182
+ [2e-4, -5e-4, 1e-4],
1183
+ [3e-4, 1e-4, 2e-4]])
1184
+ g_v, t_v = model.correct_hkls_latc(latc, strain=v)
1185
+ g_S, t_S = model.correct_hkls_latc(latc, strain=S)
1186
+ torch.testing.assert_close(g_v, g_S)
1187
+ torch.testing.assert_close(t_v, t_S)
1188
+
1189
+ def test_3x3_strain_equals_voigt_in_forward(self, nf_geometry, device):
1190
+ model, _, _ = make_model_with_cubic_iron(nf_geometry, device)
1191
+ eul = torch.tensor([[0.3, 0.5, 1.1]])
1192
+ pos = torch.zeros(1, 3)
1193
+ latc = torch.tensor([2.87, 2.87, 2.87, 90., 90., 90.]).expand(1, 6)
1194
+ v = torch.tensor([[1e-3, 2e-4, 3e-4, -5e-4, 1e-4, 2e-4]])
1195
+ S = torch.tensor([[[1e-3, 2e-4, 3e-4],
1196
+ [2e-4, -5e-4, 1e-4],
1197
+ [3e-4, 1e-4, 2e-4]]])
1198
+ sp_v = model(eul, pos, lattice_params=latc, strain=v)
1199
+ sp_S = model(eul, pos, lattice_params=latc, strain=S)
1200
+ torch.testing.assert_close(sp_v.two_theta, sp_S.two_theta)
1201
+ torch.testing.assert_close(sp_v.omega, sp_S.omega)
1202
+
1203
+
1114
1204
  if __name__ == "__main__":
1115
1205
  pytest.main([__file__, "-v"])
File without changes
File without changes
File without changes