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.
- midas_dct_tt-0.1.0/LICENSE +53 -0
- midas_dct_tt-0.1.0/PKG-INFO +237 -0
- midas_dct_tt-0.1.0/README.md +208 -0
- midas_dct_tt-0.1.0/midas_dct_tt/__init__.py +334 -0
- midas_dct_tt-0.1.0/midas_dct_tt/acceptance.py +615 -0
- midas_dct_tt-0.1.0/midas_dct_tt/chain.py +429 -0
- midas_dct_tt-0.1.0/midas_dct_tt/conventions.py +298 -0
- midas_dct_tt-0.1.0/midas_dct_tt/detector.py +357 -0
- midas_dct_tt-0.1.0/midas_dct_tt/esrf.py +425 -0
- midas_dct_tt-0.1.0/midas_dct_tt/extinction.py +264 -0
- midas_dct_tt-0.1.0/midas_dct_tt/field_inverse.py +558 -0
- midas_dct_tt-0.1.0/midas_dct_tt/forward.py +355 -0
- midas_dct_tt-0.1.0/midas_dct_tt/geometry.py +281 -0
- midas_dct_tt-0.1.0/midas_dct_tt/goniometer.py +251 -0
- midas_dct_tt-0.1.0/midas_dct_tt/grain.py +181 -0
- midas_dct_tt-0.1.0/midas_dct_tt/inverse.py +268 -0
- midas_dct_tt-0.1.0/midas_dct_tt/io.py +242 -0
- midas_dct_tt-0.1.0/midas_dct_tt/pairing.py +156 -0
- midas_dct_tt-0.1.0/midas_dct_tt/parity.py +147 -0
- midas_dct_tt-0.1.0/midas_dct_tt/planning.py +471 -0
- midas_dct_tt-0.1.0/midas_dct_tt/project.py +236 -0
- midas_dct_tt-0.1.0/midas_dct_tt/recon.py +261 -0
- midas_dct_tt-0.1.0/midas_dct_tt/rotation_coverage.py +105 -0
- midas_dct_tt-0.1.0/midas_dct_tt/scan.py +187 -0
- midas_dct_tt-0.1.0/midas_dct_tt/validate.py +91 -0
- midas_dct_tt-0.1.0/midas_dct_tt.egg-info/PKG-INFO +237 -0
- midas_dct_tt-0.1.0/midas_dct_tt.egg-info/SOURCES.txt +57 -0
- midas_dct_tt-0.1.0/midas_dct_tt.egg-info/dependency_links.txt +1 -0
- midas_dct_tt-0.1.0/midas_dct_tt.egg-info/requires.txt +15 -0
- midas_dct_tt-0.1.0/midas_dct_tt.egg-info/top_level.txt +1 -0
- midas_dct_tt-0.1.0/pyproject.toml +58 -0
- midas_dct_tt-0.1.0/setup.cfg +4 -0
- midas_dct_tt-0.1.0/tests/test_acceptance.py +545 -0
- midas_dct_tt-0.1.0/tests/test_chain.py +440 -0
- midas_dct_tt-0.1.0/tests/test_conventions.py +277 -0
- midas_dct_tt-0.1.0/tests/test_dependency_contract.py +64 -0
- midas_dct_tt-0.1.0/tests/test_detector.py +333 -0
- midas_dct_tt-0.1.0/tests/test_dfxm_contract.py +192 -0
- midas_dct_tt-0.1.0/tests/test_esrf.py +226 -0
- midas_dct_tt-0.1.0/tests/test_esrf_loader.py +89 -0
- midas_dct_tt-0.1.0/tests/test_extinction.py +203 -0
- midas_dct_tt-0.1.0/tests/test_extinction_contrast.py +210 -0
- midas_dct_tt-0.1.0/tests/test_field_inverse.py +384 -0
- midas_dct_tt-0.1.0/tests/test_forward.py +449 -0
- midas_dct_tt-0.1.0/tests/test_geometry.py +232 -0
- midas_dct_tt-0.1.0/tests/test_goniometer.py +239 -0
- midas_dct_tt-0.1.0/tests/test_grain.py +284 -0
- midas_dct_tt-0.1.0/tests/test_inverse.py +246 -0
- midas_dct_tt-0.1.0/tests/test_missing_cone.py +173 -0
- midas_dct_tt-0.1.0/tests/test_pairing.py +209 -0
- midas_dct_tt-0.1.0/tests/test_parity.py +103 -0
- midas_dct_tt-0.1.0/tests/test_planning.py +331 -0
- midas_dct_tt-0.1.0/tests/test_project.py +336 -0
- midas_dct_tt-0.1.0/tests/test_pymicro_crosscheck.py +142 -0
- midas_dct_tt-0.1.0/tests/test_radon_crosscheck.py +145 -0
- midas_dct_tt-0.1.0/tests/test_recon.py +229 -0
- midas_dct_tt-0.1.0/tests/test_rotation_coverage.py +116 -0
- midas_dct_tt-0.1.0/tests/test_scan.py +169 -0
- 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`.
|