midas-dct-tt 0.1.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 (59) hide show
  1. midas_dct_tt-0.1.0/LICENSE +53 -0
  2. midas_dct_tt-0.1.0/PKG-INFO +237 -0
  3. midas_dct_tt-0.1.0/README.md +208 -0
  4. midas_dct_tt-0.1.0/midas_dct_tt/__init__.py +334 -0
  5. midas_dct_tt-0.1.0/midas_dct_tt/acceptance.py +615 -0
  6. midas_dct_tt-0.1.0/midas_dct_tt/chain.py +429 -0
  7. midas_dct_tt-0.1.0/midas_dct_tt/conventions.py +298 -0
  8. midas_dct_tt-0.1.0/midas_dct_tt/detector.py +357 -0
  9. midas_dct_tt-0.1.0/midas_dct_tt/esrf.py +425 -0
  10. midas_dct_tt-0.1.0/midas_dct_tt/extinction.py +264 -0
  11. midas_dct_tt-0.1.0/midas_dct_tt/field_inverse.py +558 -0
  12. midas_dct_tt-0.1.0/midas_dct_tt/forward.py +355 -0
  13. midas_dct_tt-0.1.0/midas_dct_tt/geometry.py +281 -0
  14. midas_dct_tt-0.1.0/midas_dct_tt/goniometer.py +251 -0
  15. midas_dct_tt-0.1.0/midas_dct_tt/grain.py +181 -0
  16. midas_dct_tt-0.1.0/midas_dct_tt/inverse.py +268 -0
  17. midas_dct_tt-0.1.0/midas_dct_tt/io.py +242 -0
  18. midas_dct_tt-0.1.0/midas_dct_tt/pairing.py +156 -0
  19. midas_dct_tt-0.1.0/midas_dct_tt/parity.py +147 -0
  20. midas_dct_tt-0.1.0/midas_dct_tt/planning.py +471 -0
  21. midas_dct_tt-0.1.0/midas_dct_tt/project.py +236 -0
  22. midas_dct_tt-0.1.0/midas_dct_tt/recon.py +261 -0
  23. midas_dct_tt-0.1.0/midas_dct_tt/rotation_coverage.py +105 -0
  24. midas_dct_tt-0.1.0/midas_dct_tt/scan.py +187 -0
  25. midas_dct_tt-0.1.0/midas_dct_tt/validate.py +91 -0
  26. midas_dct_tt-0.1.0/midas_dct_tt.egg-info/PKG-INFO +237 -0
  27. midas_dct_tt-0.1.0/midas_dct_tt.egg-info/SOURCES.txt +57 -0
  28. midas_dct_tt-0.1.0/midas_dct_tt.egg-info/dependency_links.txt +1 -0
  29. midas_dct_tt-0.1.0/midas_dct_tt.egg-info/requires.txt +15 -0
  30. midas_dct_tt-0.1.0/midas_dct_tt.egg-info/top_level.txt +1 -0
  31. midas_dct_tt-0.1.0/pyproject.toml +58 -0
  32. midas_dct_tt-0.1.0/setup.cfg +4 -0
  33. midas_dct_tt-0.1.0/tests/test_acceptance.py +545 -0
  34. midas_dct_tt-0.1.0/tests/test_chain.py +440 -0
  35. midas_dct_tt-0.1.0/tests/test_conventions.py +277 -0
  36. midas_dct_tt-0.1.0/tests/test_dependency_contract.py +64 -0
  37. midas_dct_tt-0.1.0/tests/test_detector.py +333 -0
  38. midas_dct_tt-0.1.0/tests/test_dfxm_contract.py +192 -0
  39. midas_dct_tt-0.1.0/tests/test_esrf.py +226 -0
  40. midas_dct_tt-0.1.0/tests/test_esrf_loader.py +89 -0
  41. midas_dct_tt-0.1.0/tests/test_extinction.py +203 -0
  42. midas_dct_tt-0.1.0/tests/test_extinction_contrast.py +210 -0
  43. midas_dct_tt-0.1.0/tests/test_field_inverse.py +384 -0
  44. midas_dct_tt-0.1.0/tests/test_forward.py +449 -0
  45. midas_dct_tt-0.1.0/tests/test_geometry.py +232 -0
  46. midas_dct_tt-0.1.0/tests/test_goniometer.py +239 -0
  47. midas_dct_tt-0.1.0/tests/test_grain.py +284 -0
  48. midas_dct_tt-0.1.0/tests/test_inverse.py +246 -0
  49. midas_dct_tt-0.1.0/tests/test_missing_cone.py +173 -0
  50. midas_dct_tt-0.1.0/tests/test_pairing.py +209 -0
  51. midas_dct_tt-0.1.0/tests/test_parity.py +103 -0
  52. midas_dct_tt-0.1.0/tests/test_planning.py +331 -0
  53. midas_dct_tt-0.1.0/tests/test_project.py +336 -0
  54. midas_dct_tt-0.1.0/tests/test_pymicro_crosscheck.py +142 -0
  55. midas_dct_tt-0.1.0/tests/test_radon_crosscheck.py +145 -0
  56. midas_dct_tt-0.1.0/tests/test_recon.py +229 -0
  57. midas_dct_tt-0.1.0/tests/test_rotation_coverage.py +116 -0
  58. midas_dct_tt-0.1.0/tests/test_scan.py +169 -0
  59. midas_dct_tt-0.1.0/tests/test_scoring.py +161 -0
@@ -0,0 +1,53 @@
1
+ Copyright (c) 2012, UChicago Argonne, LLC
2
+
3
+ All Rights Reserved
4
+
5
+ MIDAS Microstructural Imaging using Diffraction Analysis Software
6
+
7
+ Materials Physics and Engineering
8
+ Computational X-ray Science
9
+ Advanced Photon Source
10
+ Argonne National Laboratory
11
+
12
+ Contributing Authors:
13
+ Hemant Sharma (hsharma@anl.gov)
14
+
15
+ OPEN SOURCE LICENSE
16
+
17
+ Redistribution and use in source and binary forms, with or without
18
+ modification, are permitted provided that the following conditions are met:
19
+
20
+ 1. Redistributions of source code must retain the above copyright notice,
21
+ this list of conditions and the following disclaimer. Software changes,
22
+ modifications, or derivative works, should be noted with comments and
23
+ the author and organization's name.
24
+
25
+ 2. Redistributions in binary form must reproduce the above copyright notice,
26
+ this list of conditions and the following disclaimer in the documentation
27
+ and/or other materials provided with the distribution.
28
+
29
+ 3. Neither the names of UChicago Argonne, LLC or the Department of Energy
30
+ nor the names of its contributors may be used to endorse or promote
31
+ products derived from this software without specific prior written
32
+ permission.
33
+
34
+ 4. The software and the end-user documentation included with the
35
+ redistribution, if any, must include the following acknowledgment:
36
+
37
+ "This product includes software produced by UChicago Argonne, LLC
38
+ under Contract No. DE-AC02-06CH11357 with the Department of Energy."
39
+
40
+ ****************************************************************************
41
+
42
+ DISCLAIMER
43
+
44
+ THE SOFTWARE IS SUPPLIED "AS IS" WITHOUT WARRANTY OF ANY KIND.
45
+
46
+ Neither the United States GOVERNMENT, nor the United States Department
47
+ of Energy, NOR UChicago Argonne, LLC, nor any of their employees, makes
48
+ any warranty, express or implied, or assumes any legal liability or
49
+ responsibility for the accuracy, completeness, or usefulness of any
50
+ information, data, apparatus, product, or process disclosed, or
51
+ represents that its use would not infringe privately owned rights.
52
+
53
+ ****************************************************************************
@@ -0,0 +1,237 @@
1
+ Metadata-Version: 2.4
2
+ Name: midas-dct-tt
3
+ Version: 0.1.0
4
+ Summary: Differentiable forward + inverse for diffraction contrast tomography (DCT) and topotomography (TT): grain-shape topographs, laminographic sampling, and joint (shape, deformation) recovery, on top of midas-dfxm / midas-diffract.
5
+ Author-email: Hemant Sharma <hsharma@anl.gov>
6
+ License-Expression: BSD-3-Clause
7
+ Classifier: Development Status :: 1 - Planning
8
+ Classifier: Intended Audience :: Science/Research
9
+ Classifier: Programming Language :: Python :: 3
10
+ Classifier: Operating System :: OS Independent
11
+ Classifier: Topic :: Scientific/Engineering :: Physics
12
+ Requires-Python: >=3.9
13
+ Description-Content-Type: text/markdown
14
+ License-File: LICENSE
15
+ Requires-Dist: numpy>=1.20
16
+ Requires-Dist: torch>=2.0
17
+ Requires-Dist: midas-dfxm>=0.3.1
18
+ Requires-Dist: midas-stress>=0.8.1
19
+ Requires-Dist: midas-diffract>=0.7
20
+ Requires-Dist: midas-hkls>=0.7.2
21
+ Requires-Dist: midas-invert>=0.1.1
22
+ Requires-Dist: midas-distortion>=0.2
23
+ Provides-Extra: viz
24
+ Requires-Dist: matplotlib>=3.5; extra == "viz"
25
+ Provides-Extra: dev
26
+ Requires-Dist: pytest>=7.0; extra == "dev"
27
+ Requires-Dist: matplotlib>=3.5; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # midas-dct-tt
31
+
32
+ Differentiable forward + inverse for **diffraction contrast tomography (DCT)** and
33
+ **topotomography (TT)**, built on `midas-dfxm` and `midas-diffract`.
34
+
35
+ > **PRIVATE / UNRELEASED.** `packages/midas_dct_tt/` is excluded in
36
+ > `.git/info/exclude` (local-only, so the ignore rule itself cannot leak the
37
+ > name), and `release.sh` refuses to run while it is. Nothing about this package — the name
38
+ > included — should reach `origin`, PyPI, a talk, or a proposal until
39
+ > [`RELEASE_CHECKLIST.md`](RELEASE_CHECKLIST.md) has been worked through
40
+ > deliberately.
41
+
42
+ ## What it is
43
+
44
+ DCT and TT are not separate physics. They are the same forward operator as DFXM
45
+ with the objective removed and the scan convention changed:
46
+
47
+ | | acceptance | projection | rotation axis | projections/grain |
48
+ | -------- | ----------------------------------- | --------------- | -------------- | ----------------- |
49
+ | **DFXM** | divergence + bandwidth + objective NA | magnified, inclined | goniometer | n/a (real-space image) |
50
+ | **TT** | divergence + bandwidth | parallel (M = 1) | **parallel to G** | 10²–10³, freely chosen |
51
+ | **DCT** | divergence + bandwidth | parallel (M = 1) | lab vertical | 10–60, crystallography-fixed |
52
+
53
+ So this package is a scan-convention layer plus a projection/reconstruction layer
54
+ over existing MIDAS primitives — not a new physics stack. The reuse map is in
55
+ `implementation_plan.md` §5, and the two reuse claims that matter (`na = 0` for
56
+ the acceptance, `M = 1` for the projection) are pinned by
57
+ [`tests/test_dfxm_contract.py`](tests/test_dfxm_contract.py) so an upstream
58
+ release cannot break them quietly.
59
+
60
+ ## The one result to know first
61
+
62
+ With the tomographic rotation axis parallel to **G** — the TT alignment — the
63
+ Bragg condition gives, exactly and for every θ:
64
+
65
+ ```
66
+ angle(k_h, a_hat) = 90° − θ
67
+ ```
68
+
69
+ Parallel-beam tomography needs the projection direction perpendicular to the
70
+ rotation axis. TT is off by exactly θ, so **TT is laminography with a missing
71
+ cone of half-angle θ**, not tomography. Consequences: low-θ (high-energy,
72
+ low-index) reflections reconstruct better; Friedel-paired settings flip the cone
73
+ and recover part of it; a reconstructed sphere elongates along the axis.
74
+
75
+ `midas_dct_tt.missing_cone_half_angle_deg` carries the full statement; the
76
+ identity is verified from vectors, independently of the θ that produced them, in
77
+ `tests/test_geometry.py`.
78
+
79
+ ## Install (editable, from the repo)
80
+
81
+ ```bash
82
+ pip install -e packages/midas_dct_tt --no-deps # siblings already editable
83
+ ```
84
+
85
+ ## Quickstart
86
+
87
+ ```python
88
+ import torch
89
+ from midas_dct_tt import sphere_grain, attach_uniform_field, tt_alignment
90
+
91
+ # A planted grain: soft voxel occupancy chi(r) + a perfect-crystal F(r).
92
+ grain = attach_uniform_field(sphere_grain(radius_um=5.0, spacing_um=0.5))
93
+
94
+ # Align its (111) so G lies on the tomographic rotation axis.
95
+ G0 = grain.field.reference_G((1, 1, 1)) # sample frame, |G| = 2*pi/d
96
+ al = tt_alignment(G0, wavelength_A=0.172979) # ~71.7 keV, 1-ID/HEXM
97
+
98
+ print(f"theta = {float(al.theta_deg):.3f} deg")
99
+ print(f"missing cone = {float(al.missing_cone_deg()):.3f} deg half-angle")
100
+ print(f"k_h to axis = {float(al.axis_beam_angle_deg()):.3f} deg (= 90 - theta)")
101
+
102
+ # The TT scan: G is invariant, so k_h never moves.
103
+ for psi in (0.0, 90.0, 180.0, 270.0):
104
+ lab_positions = grain.positions_lab(al.sample_rotation(psi))
105
+ ```
106
+
107
+ Phase 1 — a topograph, and a full sinogram:
108
+
109
+ ```python
110
+ from midas_dct_tt import PlaneDetector, psi_scan, topograph_image, topograph_stack, tt_resolution
111
+
112
+ det = PlaneDetector(pixel_um=0.5, shape=(96, 96), distance_um=5000.0)
113
+
114
+ # Ideal grain: the pixel reads path length in micrometers (centre = 2R).
115
+ img = topograph_image(grain, al, psi_deg=0.0, detector=det)
116
+
117
+ # With acceptance and deformation: intensity AND position respond to F(r).
118
+ res = tt_resolution(al) # DFXM's resolution at na = 0
119
+ sino = topograph_stack(grain, al, psi_scan(180), detector=det,
120
+ hkl=(1, 1, 1), resolution=res) # (180, 96, 96)
121
+ ```
122
+
123
+ DCT instead — solve the Bragg flashes, then render frames:
124
+
125
+ ```python
126
+ from midas_dct_tt import bragg_flashes, dct_frames, dct_omega_scan
127
+
128
+ flashes = bragg_flashes(G0, wavelength_A=0.172979) # 2 per 360 deg, or [] if blind
129
+ frames = dct_frames([grain], [flashes], wavelength_A=0.172979,
130
+ detector=PlaneDetector(pixel_um=1.0, shape=(192, 192), distance_um=500.0),
131
+ omega_centres=dct_omega_scan(720))
132
+ ```
133
+
134
+ Phase 2 — reconstruct the grain shape from the sinogram:
135
+
136
+ ```python
137
+ from midas_dct_tt import forward_operator, sirt, reconstruct_differentiable, dice
138
+
139
+ A = forward_operator(grain.positions, al, psi_scan(36), det,
140
+ voxel_volume_um3=grain.voxel_volume_um3)
141
+ sino = A(grain.occupancy) # linear in chi: no sigmoid, no acceptance
142
+
143
+ chi_sirt = sirt(sino, A, grain.n_voxels, n_iter=60) # classical baseline
144
+ chi_grad, info = reconstruct_differentiable(sino, A, grain.n_voxels, steps=300)
145
+ print(dice(chi_sirt, grain.occupancy), dice(chi_grad, grain.occupancy))
146
+ ```
147
+
148
+ ## Conventions
149
+
150
+ Inherited unchanged from `midas_dfxm` so a grain, a field, and a reflection mean
151
+ the same thing in both packages:
152
+
153
+ - lab **x** along the incident beam, **z** vertical up, `y = z × x`;
154
+ - `k_in = (2π/λ) x̂`, and reciprocal vectors carry the 2π convention (`|G| = 2π/d`);
155
+ - `v_lab = R @ v_sample`;
156
+ - **micrometers, degrees, Ångström** (wavelength and lattice only).
157
+
158
+ Two conventions are hazards rather than choices, and both are named constants
159
+ with loud docstrings rather than literals:
160
+
161
+ - **DCT ω sign.** The 1-ID aero stage is clockwise, so recorded ω must be
162
+ negated (`DCT_OMEGA_SIGN_AERO`). A wrong sign here does not degrade any
163
+ residual — it *mirrors* the reconstruction, undetectably.
164
+ - **Soft-boundary volume bias.** Occupancy is `sigmoid(−sdf/w)`, which
165
+ over-counts volume on a convex surface by `A·(2H)·w²·π²/6`. Measured, ∝ w²,
166
+ and it *inverts* below `w ≈ 0.25 × spacing` where the grid stops resolving the
167
+ ramp. Do not quote a grain volume from a soft occupancy without correcting or
168
+ stating `w`. See `logits_from_signed_distance`.
169
+
170
+ ## Status
171
+
172
+ **Phases 0-3 complete** — conventions/geometry/containers; the forward model (TT
173
+ topographs, DCT frames, acceptance, extinction, projector); shape reconstruction
174
+ (SIRT + differentiable, Friedel pairing, missing-cone study); and the deformation
175
+ inverse with a pre-registered identifiability study. 343 tests, all three device backends verified: CPU + MPS on macOS,
176
+ **CUDA on an H200** (sentosa, torch 2.11/cu128, PYTHONPATH overlay on the shared
177
+ MIDAS env).
178
+
179
+ Validated against two independent implementations:
180
+
181
+ - **`skimage.transform.radon`** for the projector — 4e-12 relative at 0°/90°,
182
+ ~0.2% RMS elsewhere (its interpolation blur, which we don't incur).
183
+ - **`pymicro`** for the DCT scan geometry — our Bragg-flash angles match
184
+ `Orientation.dct_omega_angles` to **1.1e-13 degrees** across 3 orientations ×
185
+ 3 reflections, with no fitted constant.
186
+
187
+ plus an internal NumPy oracle to 1e-13. A sphere projects to its chord, `2R`, and
188
+ projected mass is conserved exactly.
189
+
190
+ > **Phase 3 refuted this package's original novelty claim.** Exact finite-strain
191
+ > inversion is *correct* — its difference from the linearised model is exactly the
192
+ > omitted `O(|H|²)` term, verified to better than 1% — but the difference reaches
193
+ > only **2.2% at |H| = 5%** and needs **|H| ≈ 24%** to reach 10%, below the
194
+ > measurement floor throughout. Separately, a fixed TT setting goes dark at
195
+ > |H| ≈ 2e-3 (rotation), 13× before the linearisation matters. Pre-registered in
196
+ > `dev/paper/PREREGISTER.md`; full result in `dev/paper/RESULTS_phase3.md`.
197
+ >
198
+ > What survives: a validated differentiable forward model, a clean identifiability
199
+ > result (**one reflection constrains 3 of 9 components of F; three non-coplanar
200
+ > give all 9; collinear add nothing**), and the finding that TT's spatial
201
+ > resolution resolves a strain sign that intensity alone cannot.
202
+
203
+ Three results that change how you should use this package:
204
+
205
+ - **A Friedel partner is a lab-frame *mirror*, not an inversion.** The two
206
+ lab-frame `G` vectors share `(x, y)` and have opposite `z` (cos = +0.79).
207
+ Pair in the **sample** frame — a lab-frame antiparallel test returns zero pairs
208
+ and fails silently. `BraggFlash` carries `G_sample` for exactly this.
209
+ - **Do not measure the missing cone by reconstructing a sphere.** It detects
210
+ nothing (Dice 1.0000, zero elongation, at any θ) because same-grid noiseless
211
+ data is an inverse crime. Measure the operator's Fourier response instead —
212
+ `tests/test_missing_cone.py` shows ~10× suppression inside the cone with the
213
+ width tracking θ.
214
+
215
+ And two from Phase 1 about reflection selection:
216
+
217
+ - **Low θ trades tomographic coverage against strain contrast.** Both scale as
218
+ cot θ: fcc (111) at 71.7 keV gives a 2.4° missing cone but σ_par/|Q| = 6.4e-3;
219
+ at 17 keV, 10.0° and 1.5e-3. Neither choice dominates, so reflection selection
220
+ wants Fisher-information design rather than a rule of thumb.
221
+ - **TT strain contrast is divergence-limited, not bandwidth-limited** at HEXM
222
+ energies (the cot²θ·div² term beats 4ε² by ~2000×). A narrower monochromator
223
+ will not sharpen it; collimation will.
224
+
225
+ ## Tests
226
+
227
+ ```bash
228
+ KMP_DUPLICATE_LIB_OK=TRUE python -m pytest tests/ -q
229
+ ```
230
+
231
+ Markers: `unit`, `contract` (sibling API surfaces), `device` (CPU/CUDA/MPS),
232
+ `autograd` (gradcheck), `slow` (deselected by default; run with `-m slow`).
233
+
234
+ `tests/test_radon_crosscheck.py` needs `scikit-image` and skips without it.
235
+ `tests/test_pymicro_crosscheck.py` uses a frozen reference table rather than
236
+ importing pymicro, which pins `numpy<2`; regenerate with
237
+ `examples/pymicro_reference.py`.
@@ -0,0 +1,208 @@
1
+ # midas-dct-tt
2
+
3
+ Differentiable forward + inverse for **diffraction contrast tomography (DCT)** and
4
+ **topotomography (TT)**, built on `midas-dfxm` and `midas-diffract`.
5
+
6
+ > **PRIVATE / UNRELEASED.** `packages/midas_dct_tt/` is excluded in
7
+ > `.git/info/exclude` (local-only, so the ignore rule itself cannot leak the
8
+ > name), and `release.sh` refuses to run while it is. Nothing about this package — the name
9
+ > included — should reach `origin`, PyPI, a talk, or a proposal until
10
+ > [`RELEASE_CHECKLIST.md`](RELEASE_CHECKLIST.md) has been worked through
11
+ > deliberately.
12
+
13
+ ## What it is
14
+
15
+ DCT and TT are not separate physics. They are the same forward operator as DFXM
16
+ with the objective removed and the scan convention changed:
17
+
18
+ | | acceptance | projection | rotation axis | projections/grain |
19
+ | -------- | ----------------------------------- | --------------- | -------------- | ----------------- |
20
+ | **DFXM** | divergence + bandwidth + objective NA | magnified, inclined | goniometer | n/a (real-space image) |
21
+ | **TT** | divergence + bandwidth | parallel (M = 1) | **parallel to G** | 10²–10³, freely chosen |
22
+ | **DCT** | divergence + bandwidth | parallel (M = 1) | lab vertical | 10–60, crystallography-fixed |
23
+
24
+ So this package is a scan-convention layer plus a projection/reconstruction layer
25
+ over existing MIDAS primitives — not a new physics stack. The reuse map is in
26
+ `implementation_plan.md` §5, and the two reuse claims that matter (`na = 0` for
27
+ the acceptance, `M = 1` for the projection) are pinned by
28
+ [`tests/test_dfxm_contract.py`](tests/test_dfxm_contract.py) so an upstream
29
+ release cannot break them quietly.
30
+
31
+ ## The one result to know first
32
+
33
+ With the tomographic rotation axis parallel to **G** — the TT alignment — the
34
+ Bragg condition gives, exactly and for every θ:
35
+
36
+ ```
37
+ angle(k_h, a_hat) = 90° − θ
38
+ ```
39
+
40
+ Parallel-beam tomography needs the projection direction perpendicular to the
41
+ rotation axis. TT is off by exactly θ, so **TT is laminography with a missing
42
+ cone of half-angle θ**, not tomography. Consequences: low-θ (high-energy,
43
+ low-index) reflections reconstruct better; Friedel-paired settings flip the cone
44
+ and recover part of it; a reconstructed sphere elongates along the axis.
45
+
46
+ `midas_dct_tt.missing_cone_half_angle_deg` carries the full statement; the
47
+ identity is verified from vectors, independently of the θ that produced them, in
48
+ `tests/test_geometry.py`.
49
+
50
+ ## Install (editable, from the repo)
51
+
52
+ ```bash
53
+ pip install -e packages/midas_dct_tt --no-deps # siblings already editable
54
+ ```
55
+
56
+ ## Quickstart
57
+
58
+ ```python
59
+ import torch
60
+ from midas_dct_tt import sphere_grain, attach_uniform_field, tt_alignment
61
+
62
+ # A planted grain: soft voxel occupancy chi(r) + a perfect-crystal F(r).
63
+ grain = attach_uniform_field(sphere_grain(radius_um=5.0, spacing_um=0.5))
64
+
65
+ # Align its (111) so G lies on the tomographic rotation axis.
66
+ G0 = grain.field.reference_G((1, 1, 1)) # sample frame, |G| = 2*pi/d
67
+ al = tt_alignment(G0, wavelength_A=0.172979) # ~71.7 keV, 1-ID/HEXM
68
+
69
+ print(f"theta = {float(al.theta_deg):.3f} deg")
70
+ print(f"missing cone = {float(al.missing_cone_deg()):.3f} deg half-angle")
71
+ print(f"k_h to axis = {float(al.axis_beam_angle_deg()):.3f} deg (= 90 - theta)")
72
+
73
+ # The TT scan: G is invariant, so k_h never moves.
74
+ for psi in (0.0, 90.0, 180.0, 270.0):
75
+ lab_positions = grain.positions_lab(al.sample_rotation(psi))
76
+ ```
77
+
78
+ Phase 1 — a topograph, and a full sinogram:
79
+
80
+ ```python
81
+ from midas_dct_tt import PlaneDetector, psi_scan, topograph_image, topograph_stack, tt_resolution
82
+
83
+ det = PlaneDetector(pixel_um=0.5, shape=(96, 96), distance_um=5000.0)
84
+
85
+ # Ideal grain: the pixel reads path length in micrometers (centre = 2R).
86
+ img = topograph_image(grain, al, psi_deg=0.0, detector=det)
87
+
88
+ # With acceptance and deformation: intensity AND position respond to F(r).
89
+ res = tt_resolution(al) # DFXM's resolution at na = 0
90
+ sino = topograph_stack(grain, al, psi_scan(180), detector=det,
91
+ hkl=(1, 1, 1), resolution=res) # (180, 96, 96)
92
+ ```
93
+
94
+ DCT instead — solve the Bragg flashes, then render frames:
95
+
96
+ ```python
97
+ from midas_dct_tt import bragg_flashes, dct_frames, dct_omega_scan
98
+
99
+ flashes = bragg_flashes(G0, wavelength_A=0.172979) # 2 per 360 deg, or [] if blind
100
+ frames = dct_frames([grain], [flashes], wavelength_A=0.172979,
101
+ detector=PlaneDetector(pixel_um=1.0, shape=(192, 192), distance_um=500.0),
102
+ omega_centres=dct_omega_scan(720))
103
+ ```
104
+
105
+ Phase 2 — reconstruct the grain shape from the sinogram:
106
+
107
+ ```python
108
+ from midas_dct_tt import forward_operator, sirt, reconstruct_differentiable, dice
109
+
110
+ A = forward_operator(grain.positions, al, psi_scan(36), det,
111
+ voxel_volume_um3=grain.voxel_volume_um3)
112
+ sino = A(grain.occupancy) # linear in chi: no sigmoid, no acceptance
113
+
114
+ chi_sirt = sirt(sino, A, grain.n_voxels, n_iter=60) # classical baseline
115
+ chi_grad, info = reconstruct_differentiable(sino, A, grain.n_voxels, steps=300)
116
+ print(dice(chi_sirt, grain.occupancy), dice(chi_grad, grain.occupancy))
117
+ ```
118
+
119
+ ## Conventions
120
+
121
+ Inherited unchanged from `midas_dfxm` so a grain, a field, and a reflection mean
122
+ the same thing in both packages:
123
+
124
+ - lab **x** along the incident beam, **z** vertical up, `y = z × x`;
125
+ - `k_in = (2π/λ) x̂`, and reciprocal vectors carry the 2π convention (`|G| = 2π/d`);
126
+ - `v_lab = R @ v_sample`;
127
+ - **micrometers, degrees, Ångström** (wavelength and lattice only).
128
+
129
+ Two conventions are hazards rather than choices, and both are named constants
130
+ with loud docstrings rather than literals:
131
+
132
+ - **DCT ω sign.** The 1-ID aero stage is clockwise, so recorded ω must be
133
+ negated (`DCT_OMEGA_SIGN_AERO`). A wrong sign here does not degrade any
134
+ residual — it *mirrors* the reconstruction, undetectably.
135
+ - **Soft-boundary volume bias.** Occupancy is `sigmoid(−sdf/w)`, which
136
+ over-counts volume on a convex surface by `A·(2H)·w²·π²/6`. Measured, ∝ w²,
137
+ and it *inverts* below `w ≈ 0.25 × spacing` where the grid stops resolving the
138
+ ramp. Do not quote a grain volume from a soft occupancy without correcting or
139
+ stating `w`. See `logits_from_signed_distance`.
140
+
141
+ ## Status
142
+
143
+ **Phases 0-3 complete** — conventions/geometry/containers; the forward model (TT
144
+ topographs, DCT frames, acceptance, extinction, projector); shape reconstruction
145
+ (SIRT + differentiable, Friedel pairing, missing-cone study); and the deformation
146
+ inverse with a pre-registered identifiability study. 343 tests, all three device backends verified: CPU + MPS on macOS,
147
+ **CUDA on an H200** (sentosa, torch 2.11/cu128, PYTHONPATH overlay on the shared
148
+ MIDAS env).
149
+
150
+ Validated against two independent implementations:
151
+
152
+ - **`skimage.transform.radon`** for the projector — 4e-12 relative at 0°/90°,
153
+ ~0.2% RMS elsewhere (its interpolation blur, which we don't incur).
154
+ - **`pymicro`** for the DCT scan geometry — our Bragg-flash angles match
155
+ `Orientation.dct_omega_angles` to **1.1e-13 degrees** across 3 orientations ×
156
+ 3 reflections, with no fitted constant.
157
+
158
+ plus an internal NumPy oracle to 1e-13. A sphere projects to its chord, `2R`, and
159
+ projected mass is conserved exactly.
160
+
161
+ > **Phase 3 refuted this package's original novelty claim.** Exact finite-strain
162
+ > inversion is *correct* — its difference from the linearised model is exactly the
163
+ > omitted `O(|H|²)` term, verified to better than 1% — but the difference reaches
164
+ > only **2.2% at |H| = 5%** and needs **|H| ≈ 24%** to reach 10%, below the
165
+ > measurement floor throughout. Separately, a fixed TT setting goes dark at
166
+ > |H| ≈ 2e-3 (rotation), 13× before the linearisation matters. Pre-registered in
167
+ > `dev/paper/PREREGISTER.md`; full result in `dev/paper/RESULTS_phase3.md`.
168
+ >
169
+ > What survives: a validated differentiable forward model, a clean identifiability
170
+ > result (**one reflection constrains 3 of 9 components of F; three non-coplanar
171
+ > give all 9; collinear add nothing**), and the finding that TT's spatial
172
+ > resolution resolves a strain sign that intensity alone cannot.
173
+
174
+ Three results that change how you should use this package:
175
+
176
+ - **A Friedel partner is a lab-frame *mirror*, not an inversion.** The two
177
+ lab-frame `G` vectors share `(x, y)` and have opposite `z` (cos = +0.79).
178
+ Pair in the **sample** frame — a lab-frame antiparallel test returns zero pairs
179
+ and fails silently. `BraggFlash` carries `G_sample` for exactly this.
180
+ - **Do not measure the missing cone by reconstructing a sphere.** It detects
181
+ nothing (Dice 1.0000, zero elongation, at any θ) because same-grid noiseless
182
+ data is an inverse crime. Measure the operator's Fourier response instead —
183
+ `tests/test_missing_cone.py` shows ~10× suppression inside the cone with the
184
+ width tracking θ.
185
+
186
+ And two from Phase 1 about reflection selection:
187
+
188
+ - **Low θ trades tomographic coverage against strain contrast.** Both scale as
189
+ cot θ: fcc (111) at 71.7 keV gives a 2.4° missing cone but σ_par/|Q| = 6.4e-3;
190
+ at 17 keV, 10.0° and 1.5e-3. Neither choice dominates, so reflection selection
191
+ wants Fisher-information design rather than a rule of thumb.
192
+ - **TT strain contrast is divergence-limited, not bandwidth-limited** at HEXM
193
+ energies (the cot²θ·div² term beats 4ε² by ~2000×). A narrower monochromator
194
+ will not sharpen it; collimation will.
195
+
196
+ ## Tests
197
+
198
+ ```bash
199
+ KMP_DUPLICATE_LIB_OK=TRUE python -m pytest tests/ -q
200
+ ```
201
+
202
+ Markers: `unit`, `contract` (sibling API surfaces), `device` (CPU/CUDA/MPS),
203
+ `autograd` (gradcheck), `slow` (deselected by default; run with `-m slow`).
204
+
205
+ `tests/test_radon_crosscheck.py` needs `scikit-image` and skips without it.
206
+ `tests/test_pymicro_crosscheck.py` uses a frozen reference table rather than
207
+ importing pymicro, which pins `numpy<2`; regenerate with
208
+ `examples/pymicro_reference.py`.