midas-2d 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_2d/__init__.py +211 -0
- midas_2d/active_learning.py +13 -0
- midas_2d/coherent.py +120 -0
- midas_2d/debye.py +166 -0
- midas_2d/disorder.py +166 -0
- midas_2d/dynamics.py +136 -0
- midas_2d/energy.py +62 -0
- midas_2d/ensemble.py +64 -0
- midas_2d/examples/__init__.py +0 -0
- midas_2d/examples/tutorial_coherent_rsm.py +84 -0
- midas_2d/examples/tutorial_depth_strain.py +103 -0
- midas_2d/examples/tutorial_frontier.py +109 -0
- midas_2d/examples/tutorial_md_transient_disorder.py +133 -0
- midas_2d/examples/tutorial_ml_and_detector.py +96 -0
- midas_2d/examples/tutorial_npl_fringes.py +52 -0
- midas_2d/examples/tutorial_stiffness_and_phonon.py +116 -0
- midas_2d/examples/tutorial_transport_and_md.py +131 -0
- midas_2d/forward.py +115 -0
- midas_2d/instrument.py +116 -0
- midas_2d/inverse.py +9 -0
- midas_2d/io.py +180 -0
- midas_2d/latent_dynamics.py +11 -0
- midas_2d/md_integrator.py +159 -0
- midas_2d/ml.py +40 -0
- midas_2d/multimodal.py +149 -0
- midas_2d/phonon.py +103 -0
- midas_2d/realdata.py +89 -0
- midas_2d/rocking.py +180 -0
- midas_2d/shape_factor.py +167 -0
- midas_2d/strain_profile.py +232 -0
- midas_2d/thermal_transport.py +182 -0
- midas_2d/viz.py +58 -0
- midas_2d-0.1.0.dist-info/METADATA +178 -0
- midas_2d-0.1.0.dist-info/RECORD +37 -0
- midas_2d-0.1.0.dist-info/WHEEL +5 -0
- midas_2d-0.1.0.dist-info/licenses/LICENSE +53 -0
- midas_2d-0.1.0.dist-info/top_level.txt +1 -0
midas_2d/__init__.py
ADDED
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""midas-2d -- differentiable diffraction for 2D / few-layer & ultrafast work.
|
|
2
|
+
|
|
3
|
+
Phase 1 (finite-size forward core):
|
|
4
|
+
from midas_2d import (
|
|
5
|
+
interference_factor, truncation_rod, nanoplatelet_rod,
|
|
6
|
+
rod_intensity, structure_factor_q,
|
|
7
|
+
cdse_zincblende, build_crystal_tensor,
|
|
8
|
+
make_spectrum, monochromatic,
|
|
9
|
+
)
|
|
10
|
+
|
|
11
|
+
Builds on midas-hkls (structure factors, form factors, lattice, hkl gen) and
|
|
12
|
+
midas-pink (energy spectrum). See MIDAS_2D_ULTRAFAST_DIFFRACTION_PLAN.md.
|
|
13
|
+
"""
|
|
14
|
+
from .coherent import bcdi_forward, coherent_speckle, phase_retrieval
|
|
15
|
+
from .debye import (
|
|
16
|
+
atomic_form_factors,
|
|
17
|
+
coherent_amplitude,
|
|
18
|
+
coherent_intensity,
|
|
19
|
+
debye_intensity,
|
|
20
|
+
ensemble_intensity,
|
|
21
|
+
)
|
|
22
|
+
from .disorder import (
|
|
23
|
+
AnisotropicMSD,
|
|
24
|
+
TransientMSD,
|
|
25
|
+
dwf_amplitude,
|
|
26
|
+
msd_tensor_from_frames,
|
|
27
|
+
)
|
|
28
|
+
from .dynamics import (
|
|
29
|
+
msd_from_stiffness,
|
|
30
|
+
recover_stiffness,
|
|
31
|
+
stiffness_from_msd,
|
|
32
|
+
thermal_ensemble,
|
|
33
|
+
)
|
|
34
|
+
from .energy import make_spectrum, monochromatic
|
|
35
|
+
from .instrument import (
|
|
36
|
+
add_poisson_noise,
|
|
37
|
+
poisson_nll,
|
|
38
|
+
project_to_detector,
|
|
39
|
+
resolution_convolve,
|
|
40
|
+
solid_angle_polarization,
|
|
41
|
+
)
|
|
42
|
+
from .inverse import cosine_loss, fit, laplace_uncertainty, relative_l2_loss
|
|
43
|
+
from .ml import ParameterMLP, make_dataset, train_surrogate
|
|
44
|
+
from .phonon import (
|
|
45
|
+
apply_out_of_plane_strain,
|
|
46
|
+
bragg_timeseries,
|
|
47
|
+
fit_coherent_phonon,
|
|
48
|
+
strain_wave,
|
|
49
|
+
)
|
|
50
|
+
from .realdata import debye_reference_numpy, load_profile
|
|
51
|
+
from .active_learning import (
|
|
52
|
+
fisher_information,
|
|
53
|
+
next_best_measurement,
|
|
54
|
+
rank_measurements,
|
|
55
|
+
sensitivity,
|
|
56
|
+
)
|
|
57
|
+
from .ensemble import polydisperse_rod, recover_thickness_distribution
|
|
58
|
+
from .latent_dynamics import discover_eom, integrate_latent_ode, library_terms
|
|
59
|
+
from .md_integrator import (
|
|
60
|
+
bragg_from_trajectory,
|
|
61
|
+
coherent_mode_kick,
|
|
62
|
+
harmonic_force,
|
|
63
|
+
recover_potential_from_movie,
|
|
64
|
+
velocity_verlet,
|
|
65
|
+
)
|
|
66
|
+
from .multimodal import (
|
|
67
|
+
carrier_density,
|
|
68
|
+
fit_multimodal,
|
|
69
|
+
lattice_temperature_from_carriers,
|
|
70
|
+
optical_signal,
|
|
71
|
+
strain_two_channel,
|
|
72
|
+
xray_only_degeneracy,
|
|
73
|
+
)
|
|
74
|
+
from .strain_profile import (
|
|
75
|
+
acoustic_pulse,
|
|
76
|
+
apply_depth_displacement,
|
|
77
|
+
depth_resolved_amplitude,
|
|
78
|
+
depth_resolved_intensity,
|
|
79
|
+
exponential_strain,
|
|
80
|
+
linear_strain,
|
|
81
|
+
recover_depth_strain,
|
|
82
|
+
strain_to_displacement,
|
|
83
|
+
temperature_to_displacement,
|
|
84
|
+
temperature_to_msd,
|
|
85
|
+
thermal_rod,
|
|
86
|
+
)
|
|
87
|
+
from .thermal_transport import (
|
|
88
|
+
fit_electron_phonon_coupling,
|
|
89
|
+
fit_thermal_diffusivity,
|
|
90
|
+
heat_diffusion_1d,
|
|
91
|
+
lattice_T_to_intensity_ratio,
|
|
92
|
+
two_temperature_model,
|
|
93
|
+
)
|
|
94
|
+
from .rocking import (
|
|
95
|
+
analytic_rod_model,
|
|
96
|
+
fwhm,
|
|
97
|
+
md_rod_model,
|
|
98
|
+
reciprocal_space_map,
|
|
99
|
+
rocking_curve,
|
|
100
|
+
thickness_from_fwhm,
|
|
101
|
+
thickness_loss_scan,
|
|
102
|
+
)
|
|
103
|
+
from .forward import rod_intensity, structure_factor_q
|
|
104
|
+
from .io import (
|
|
105
|
+
build_crystal_tensor,
|
|
106
|
+
cdse_supercell,
|
|
107
|
+
cdse_zincblende,
|
|
108
|
+
load_xyz_frames,
|
|
109
|
+
supercell_from_crystal,
|
|
110
|
+
)
|
|
111
|
+
from .shape_factor import (
|
|
112
|
+
interference_factor,
|
|
113
|
+
laue_interference_1d,
|
|
114
|
+
nanoplatelet_rod,
|
|
115
|
+
truncation_rod,
|
|
116
|
+
)
|
|
117
|
+
|
|
118
|
+
__version__ = "0.1.0"
|
|
119
|
+
|
|
120
|
+
__all__ = [
|
|
121
|
+
"interference_factor",
|
|
122
|
+
"laue_interference_1d",
|
|
123
|
+
"nanoplatelet_rod",
|
|
124
|
+
"truncation_rod",
|
|
125
|
+
"rod_intensity",
|
|
126
|
+
"structure_factor_q",
|
|
127
|
+
"cdse_zincblende",
|
|
128
|
+
"build_crystal_tensor",
|
|
129
|
+
"cdse_supercell",
|
|
130
|
+
"supercell_from_crystal",
|
|
131
|
+
"load_xyz_frames",
|
|
132
|
+
"atomic_form_factors",
|
|
133
|
+
"coherent_amplitude",
|
|
134
|
+
"coherent_intensity",
|
|
135
|
+
"debye_intensity",
|
|
136
|
+
"ensemble_intensity",
|
|
137
|
+
"AnisotropicMSD",
|
|
138
|
+
"TransientMSD",
|
|
139
|
+
"dwf_amplitude",
|
|
140
|
+
"msd_tensor_from_frames",
|
|
141
|
+
"analytic_rod_model",
|
|
142
|
+
"md_rod_model",
|
|
143
|
+
"rocking_curve",
|
|
144
|
+
"fwhm",
|
|
145
|
+
"thickness_from_fwhm",
|
|
146
|
+
"reciprocal_space_map",
|
|
147
|
+
"thickness_loss_scan",
|
|
148
|
+
"fit",
|
|
149
|
+
"laplace_uncertainty",
|
|
150
|
+
"relative_l2_loss",
|
|
151
|
+
"cosine_loss",
|
|
152
|
+
"coherent_speckle",
|
|
153
|
+
"bcdi_forward",
|
|
154
|
+
"phase_retrieval",
|
|
155
|
+
"stiffness_from_msd",
|
|
156
|
+
"msd_from_stiffness",
|
|
157
|
+
"thermal_ensemble",
|
|
158
|
+
"recover_stiffness",
|
|
159
|
+
"strain_wave",
|
|
160
|
+
"apply_out_of_plane_strain",
|
|
161
|
+
"bragg_timeseries",
|
|
162
|
+
"fit_coherent_phonon",
|
|
163
|
+
"make_dataset",
|
|
164
|
+
"ParameterMLP",
|
|
165
|
+
"train_surrogate",
|
|
166
|
+
"project_to_detector",
|
|
167
|
+
"solid_angle_polarization",
|
|
168
|
+
"poisson_nll",
|
|
169
|
+
"add_poisson_noise",
|
|
170
|
+
"resolution_convolve",
|
|
171
|
+
"load_profile",
|
|
172
|
+
"debye_reference_numpy",
|
|
173
|
+
"depth_resolved_amplitude",
|
|
174
|
+
"depth_resolved_intensity",
|
|
175
|
+
"apply_depth_displacement",
|
|
176
|
+
"linear_strain",
|
|
177
|
+
"exponential_strain",
|
|
178
|
+
"acoustic_pulse",
|
|
179
|
+
"strain_to_displacement",
|
|
180
|
+
"temperature_to_displacement",
|
|
181
|
+
"temperature_to_msd",
|
|
182
|
+
"thermal_rod",
|
|
183
|
+
"recover_depth_strain",
|
|
184
|
+
"two_temperature_model",
|
|
185
|
+
"lattice_T_to_intensity_ratio",
|
|
186
|
+
"fit_electron_phonon_coupling",
|
|
187
|
+
"heat_diffusion_1d",
|
|
188
|
+
"fit_thermal_diffusivity",
|
|
189
|
+
"harmonic_force",
|
|
190
|
+
"velocity_verlet",
|
|
191
|
+
"bragg_from_trajectory",
|
|
192
|
+
"coherent_mode_kick",
|
|
193
|
+
"recover_potential_from_movie",
|
|
194
|
+
"carrier_density",
|
|
195
|
+
"lattice_temperature_from_carriers",
|
|
196
|
+
"strain_two_channel",
|
|
197
|
+
"optical_signal",
|
|
198
|
+
"fit_multimodal",
|
|
199
|
+
"xray_only_degeneracy",
|
|
200
|
+
"library_terms",
|
|
201
|
+
"integrate_latent_ode",
|
|
202
|
+
"discover_eom",
|
|
203
|
+
"polydisperse_rod",
|
|
204
|
+
"recover_thickness_distribution",
|
|
205
|
+
"sensitivity",
|
|
206
|
+
"fisher_information",
|
|
207
|
+
"rank_measurements",
|
|
208
|
+
"next_best_measurement",
|
|
209
|
+
"make_spectrum",
|
|
210
|
+
"monochromatic",
|
|
211
|
+
]
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Experiment design -- now provided by the shared ``midas_invert`` leaf.
|
|
2
|
+
|
|
3
|
+
Thin re-export so existing ``midas_2d`` imports are unchanged.
|
|
4
|
+
"""
|
|
5
|
+
from midas_invert.design import (
|
|
6
|
+
fisher_information,
|
|
7
|
+
next_best_measurement,
|
|
8
|
+
rank_measurements,
|
|
9
|
+
sensitivity,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__all__ = ["sensitivity", "fisher_information", "rank_measurements",
|
|
13
|
+
"next_best_measurement"]
|
midas_2d/coherent.py
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
"""Coherent diffraction & differentiable phase retrieval (BCDI thrust).
|
|
2
|
+
|
|
3
|
+
Two faces of coherence:
|
|
4
|
+
|
|
5
|
+
* :func:`coherent_speckle` -- finite-size *speckle* straight from atomic
|
|
6
|
+
coordinates: ``|A(Q)|^2`` on a 2-D/3-D Q grid (reuses
|
|
7
|
+
:func:`midas_2d.debye.coherent_intensity`). This is the coordinate-level
|
|
8
|
+
coherent forward, differentiable w.r.t. positions.
|
|
9
|
+
|
|
10
|
+
* :func:`bcdi_forward` + :func:`phase_retrieval` -- the object-level BCDI model
|
|
11
|
+
used by the Hruszkewycz/Cherukara thrust: a complex object
|
|
12
|
+
``psi(r) = rho(r) exp(i Q0 . u(r))`` (amplitude = electron density / support,
|
|
13
|
+
phase = lattice displacement projected on the Bragg vector) produces speckle
|
|
14
|
+
``|FFT(psi)|^2``. :func:`phase_retrieval` inverts it by autograd -- the
|
|
15
|
+
differentiable alternative to iterative ER/HIO, and the natural place to drop
|
|
16
|
+
in a learned (Cherukara-style) prior.
|
|
17
|
+
|
|
18
|
+
All complex-valued and torch-differentiable; CPU/CUDA/MPS portable.
|
|
19
|
+
"""
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
from typing import TYPE_CHECKING
|
|
23
|
+
|
|
24
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
25
|
+
import torch
|
|
26
|
+
|
|
27
|
+
__all__ = [
|
|
28
|
+
"coherent_speckle",
|
|
29
|
+
"bcdi_forward",
|
|
30
|
+
"phase_retrieval",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def coherent_speckle(coords, elements, q_grid):
|
|
35
|
+
"""``|A(Q)|^2`` over an arbitrary grid of Q-vectors (shape (..., 3))."""
|
|
36
|
+
from .debye import coherent_intensity
|
|
37
|
+
return coherent_intensity(coords, elements, q_grid)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def bcdi_forward(psi, *, shift=True):
|
|
41
|
+
"""Far-field coherent intensity ``|FFT(psi)|^2`` of a complex object.
|
|
42
|
+
|
|
43
|
+
Parameters
|
|
44
|
+
----------
|
|
45
|
+
psi : complex tensor
|
|
46
|
+
The complex object (1-, 2-, or 3-D).
|
|
47
|
+
shift : bool
|
|
48
|
+
Apply ``fftshift`` so the zero frequency is centred.
|
|
49
|
+
|
|
50
|
+
Returns
|
|
51
|
+
-------
|
|
52
|
+
real tensor, same shape as ``psi``.
|
|
53
|
+
"""
|
|
54
|
+
import torch
|
|
55
|
+
F = torch.fft.fftn(psi)
|
|
56
|
+
if shift:
|
|
57
|
+
F = torch.fft.fftshift(F)
|
|
58
|
+
return F.real * F.real + F.imag * F.imag
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def phase_retrieval(measured_intensity, support, *, init=None, steps=600,
|
|
62
|
+
lr=0.05, beta_tv=0.0):
|
|
63
|
+
"""Differentiable (autograd) phase retrieval of a complex object.
|
|
64
|
+
|
|
65
|
+
Minimises ``|| |FFT(psi)|^2 - measured ||^2`` over a complex object
|
|
66
|
+
constrained to live inside ``support`` (a 0/1 mask, applied every forward
|
|
67
|
+
so the constraint is exact and differentiable).
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
measured_intensity : real tensor
|
|
72
|
+
Measured speckle ``|FFT(psi_true)|^2`` (fftshifted), same shape as the
|
|
73
|
+
object.
|
|
74
|
+
support : real tensor (0/1)
|
|
75
|
+
Object-domain support mask.
|
|
76
|
+
init : complex tensor, optional
|
|
77
|
+
Starting object; defaults to a random object inside the support seeded
|
|
78
|
+
from the measured magnitude.
|
|
79
|
+
steps, lr : optimisation controls.
|
|
80
|
+
beta_tv : float
|
|
81
|
+
Optional total-variation weight on |psi| (smoothness prior).
|
|
82
|
+
|
|
83
|
+
Returns
|
|
84
|
+
-------
|
|
85
|
+
dict: ``psi`` (recovered complex object), ``history`` (loss list).
|
|
86
|
+
"""
|
|
87
|
+
import torch
|
|
88
|
+
|
|
89
|
+
measured = torch.as_tensor(measured_intensity)
|
|
90
|
+
support = torch.as_tensor(support, dtype=measured.dtype, device=measured.device)
|
|
91
|
+
|
|
92
|
+
if init is None:
|
|
93
|
+
# Random phase on a magnitude guess derived from the measured speckle.
|
|
94
|
+
mag = torch.sqrt(torch.clamp(measured.mean()) + 0.0) * support
|
|
95
|
+
phase = 2 * torch.pi * torch.rand(measured.shape, device=measured.device,
|
|
96
|
+
dtype=measured.dtype)
|
|
97
|
+
init = torch.complex(mag * torch.cos(phase), mag * torch.sin(phase))
|
|
98
|
+
|
|
99
|
+
re = (init.real * support).clone().detach().requires_grad_(True)
|
|
100
|
+
im = (init.imag * support).clone().detach().requires_grad_(True)
|
|
101
|
+
opt = torch.optim.Adam([re, im], lr=lr)
|
|
102
|
+
|
|
103
|
+
history: list[float] = []
|
|
104
|
+
obs_norm = measured.pow(2).sum() + 1e-12
|
|
105
|
+
for _ in range(steps):
|
|
106
|
+
opt.zero_grad()
|
|
107
|
+
psi = torch.complex(re * support, im * support) # hard support constraint
|
|
108
|
+
pred = bcdi_forward(psi)
|
|
109
|
+
loss = ((pred - measured) ** 2).sum() / obs_norm
|
|
110
|
+
if beta_tv > 0:
|
|
111
|
+
amp = psi.abs()
|
|
112
|
+
tv = sum((torch.diff(amp, dim=d).abs().sum()
|
|
113
|
+
for d in range(amp.dim())))
|
|
114
|
+
loss = loss + beta_tv * tv
|
|
115
|
+
loss.backward()
|
|
116
|
+
opt.step()
|
|
117
|
+
history.append(float(loss))
|
|
118
|
+
|
|
119
|
+
psi = torch.complex(re * support, im * support).detach()
|
|
120
|
+
return {"psi": psi, "history": history}
|
midas_2d/debye.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""MD-coupled differentiable diffraction: scatter straight from atomic coordinates.
|
|
2
|
+
|
|
3
|
+
Two forward paths, both autograd-differentiable **w.r.t. the atomic coordinates
|
|
4
|
+
themselves** (this is the novel hook -- gradients flow from a measured pattern
|
|
5
|
+
back to per-atom positions / displacement statistics, and onward to an MD force
|
|
6
|
+
field):
|
|
7
|
+
|
|
8
|
+
* :func:`coherent_amplitude` -- oriented, complex
|
|
9
|
+
A(Q) = sum_i f_i(|Q|) exp(i Q . r_i), I = |A|^2
|
|
10
|
+
Sampling ``Q`` along c* reproduces the analytic shape-function fringes of
|
|
11
|
+
Phase 1 (see the cross-check test) -- but here the fringes, the thermal
|
|
12
|
+
Debye-Waller falloff, and any *anisotropic* disordering all emerge from the
|
|
13
|
+
coordinates rather than from a phenomenological factor.
|
|
14
|
+
|
|
15
|
+
* :func:`debye_intensity` -- orientationally averaged (the colloidal-solution
|
|
16
|
+
case for the Schaller/Flanders nanoplatelets)
|
|
17
|
+
I(q) = sum_i sum_j f_i(q) f_j(q) sinc(q r_ij)
|
|
18
|
+
|
|
19
|
+
:func:`ensemble_intensity` averages the incoherent intensity over MD frames;
|
|
20
|
+
the spread of atomic positions across the trajectory *is* the disorder, so the
|
|
21
|
+
anisotropic out-of-plane vs in-plane Debye-Waller falloff is reproduced with no
|
|
22
|
+
DWF assumption at all.
|
|
23
|
+
|
|
24
|
+
Form factors come from ``midas_hkls`` (Cromer-Mann), evaluated at the physical
|
|
25
|
+
``s = |Q| / (4 pi)``.
|
|
26
|
+
"""
|
|
27
|
+
from __future__ import annotations
|
|
28
|
+
|
|
29
|
+
import math
|
|
30
|
+
from typing import TYPE_CHECKING, Sequence
|
|
31
|
+
|
|
32
|
+
from midas_hkls.form_factors import form_factor_batch
|
|
33
|
+
|
|
34
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
35
|
+
import torch
|
|
36
|
+
|
|
37
|
+
__all__ = [
|
|
38
|
+
"atomic_form_factors",
|
|
39
|
+
"coherent_amplitude",
|
|
40
|
+
"coherent_intensity",
|
|
41
|
+
"debye_intensity",
|
|
42
|
+
"ensemble_intensity",
|
|
43
|
+
]
|
|
44
|
+
|
|
45
|
+
_FOUR_PI = 4.0 * math.pi
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def atomic_form_factors(elements, q_mag):
|
|
49
|
+
"""Per-atom Cromer-Mann form factors at scattering magnitude ``q_mag`` (1/A).
|
|
50
|
+
|
|
51
|
+
``q_mag`` may have any shape; returns shape ``(*q_mag.shape, M)`` with M the
|
|
52
|
+
number of atoms in ``elements``.
|
|
53
|
+
"""
|
|
54
|
+
import torch
|
|
55
|
+
q_mag = torch.as_tensor(q_mag)
|
|
56
|
+
s2 = (q_mag / _FOUR_PI) ** 2 # s = |Q|/(4 pi)
|
|
57
|
+
return form_factor_batch(s2, list(elements)) # (..., M)
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def coherent_amplitude(coords, elements, q_vec):
|
|
61
|
+
"""Complex coherent amplitude ``A(Q) = sum_i f_i(|Q|) exp(i Q . r_i)``.
|
|
62
|
+
|
|
63
|
+
Parameters
|
|
64
|
+
----------
|
|
65
|
+
coords : tensor (M, 3)
|
|
66
|
+
Cartesian atomic coordinates (Angstrom). Differentiable.
|
|
67
|
+
elements : sequence[str], length M
|
|
68
|
+
q_vec : tensor (..., 3)
|
|
69
|
+
Scattering vectors ``Q = 2 pi (h a* + k b* + l c*)`` in 1/A.
|
|
70
|
+
|
|
71
|
+
Returns
|
|
72
|
+
-------
|
|
73
|
+
tensor (...) complex
|
|
74
|
+
"""
|
|
75
|
+
import torch
|
|
76
|
+
coords = torch.as_tensor(coords)
|
|
77
|
+
q_vec = torch.as_tensor(q_vec, dtype=coords.dtype, device=coords.device)
|
|
78
|
+
qmag = torch.linalg.vector_norm(q_vec, dim=-1) # (...)
|
|
79
|
+
f = atomic_form_factors(elements, qmag) # (..., M)
|
|
80
|
+
phase = q_vec @ coords.T # (..., M)
|
|
81
|
+
re = (f * torch.cos(phase)).sum(dim=-1)
|
|
82
|
+
im = (f * torch.sin(phase)).sum(dim=-1)
|
|
83
|
+
return torch.complex(re, im)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
def coherent_intensity(coords, elements, q_vec):
|
|
87
|
+
"""``|A(Q)|^2`` for the coherent (oriented) path."""
|
|
88
|
+
A = coherent_amplitude(coords, elements, q_vec)
|
|
89
|
+
return A.real * A.real + A.imag * A.imag
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def debye_intensity(coords, elements, q_mag, *, tile=2048):
|
|
93
|
+
"""Orientationally-averaged intensity via the Debye scattering equation.
|
|
94
|
+
|
|
95
|
+
I(q) = sum_i sum_j f_i(q) f_j(q) sinc(q r_ij), sinc(x)=sin(x)/x
|
|
96
|
+
|
|
97
|
+
Parameters
|
|
98
|
+
----------
|
|
99
|
+
coords : tensor (M, 3)
|
|
100
|
+
Cartesian coordinates (A). Differentiable.
|
|
101
|
+
elements : sequence[str], length M
|
|
102
|
+
q_mag : tensor (Q,)
|
|
103
|
+
Scattering-vector magnitudes |Q| (1/A).
|
|
104
|
+
tile : int
|
|
105
|
+
Row-tile size over atoms to bound memory (the pair matrix is M x M).
|
|
106
|
+
|
|
107
|
+
Returns
|
|
108
|
+
-------
|
|
109
|
+
tensor (Q,)
|
|
110
|
+
"""
|
|
111
|
+
import torch
|
|
112
|
+
coords = torch.as_tensor(coords)
|
|
113
|
+
q_mag = torch.as_tensor(q_mag, dtype=coords.dtype, device=coords.device)
|
|
114
|
+
M = coords.shape[0]
|
|
115
|
+
f = atomic_form_factors(elements, q_mag) # (Q, M)
|
|
116
|
+
|
|
117
|
+
out = torch.zeros(q_mag.shape, dtype=coords.dtype, device=coords.device)
|
|
118
|
+
for start in range(0, M, tile):
|
|
119
|
+
stop = min(start + tile, M)
|
|
120
|
+
ci = coords[start:stop] # (b, 3)
|
|
121
|
+
# pairwise distances block (b, M)
|
|
122
|
+
rij = torch.cdist(ci, coords) # (b, M)
|
|
123
|
+
qr = q_mag[:, None, None] * rij[None, :, :] # (Q, b, M)
|
|
124
|
+
sinc = torch.sinc(qr / math.pi) # sin(qr)/(qr), =1 at 0
|
|
125
|
+
fi = f[:, start:stop] # (Q, b)
|
|
126
|
+
# sum_j f_j sinc -> (Q, b); then weight by f_i and sum over i
|
|
127
|
+
contrib = (fi * torch.einsum('qm,qbm->qb', f, sinc)).sum(dim=-1)
|
|
128
|
+
out = out + contrib
|
|
129
|
+
return out
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def ensemble_intensity(frames, elements, q, *, coherent=False, q_is_vector=None,
|
|
133
|
+
tile=2048):
|
|
134
|
+
"""Average intensity over a stack of MD frames (the disorder = the spread).
|
|
135
|
+
|
|
136
|
+
Parameters
|
|
137
|
+
----------
|
|
138
|
+
frames : tensor (F, M, 3)
|
|
139
|
+
F snapshots of M atoms (Angstrom).
|
|
140
|
+
elements : sequence[str], length M
|
|
141
|
+
q : tensor
|
|
142
|
+
``(Q, 3)`` Q-vectors if ``coherent`` (or ``q_is_vector``), else ``(Q,)``
|
|
143
|
+
magnitudes for the Debye path.
|
|
144
|
+
coherent : bool
|
|
145
|
+
If True, average ``|A(Q)|^2`` over frames (oriented). Else average the
|
|
146
|
+
Debye powder intensity.
|
|
147
|
+
|
|
148
|
+
Returns
|
|
149
|
+
-------
|
|
150
|
+
tensor (Q,)
|
|
151
|
+
Frame-averaged intensity.
|
|
152
|
+
"""
|
|
153
|
+
import torch
|
|
154
|
+
frames = torch.as_tensor(frames)
|
|
155
|
+
if frames.dim() == 2:
|
|
156
|
+
frames = frames.unsqueeze(0)
|
|
157
|
+
use_vec = coherent if q_is_vector is None else q_is_vector
|
|
158
|
+
|
|
159
|
+
acc = None
|
|
160
|
+
for fr in frames:
|
|
161
|
+
if use_vec:
|
|
162
|
+
I = coherent_intensity(fr, elements, q)
|
|
163
|
+
else:
|
|
164
|
+
I = debye_intensity(fr, elements, q, tile=tile)
|
|
165
|
+
acc = I if acc is None else acc + I
|
|
166
|
+
return acc / frames.shape[0]
|
midas_2d/disorder.py
ADDED
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
"""Transient, anisotropic disorder for few-layer materials.
|
|
2
|
+
|
|
3
|
+
Schaller/Flanders observe that optical excitation disorders a CdSe nanoplatelet
|
|
4
|
+
*anisotropically* -- the out-of-plane mean-square displacement grows differently
|
|
5
|
+
from the in-plane one, and it evolves with pump-probe delay. This module gives
|
|
6
|
+
that signature two equivalent faces:
|
|
7
|
+
|
|
8
|
+
1. **Analytic Debye-Waller** (:class:`AnisotropicMSD`, :class:`TransientMSD`):
|
|
9
|
+
a differentiable temperature factor ``T(Q) = exp(-1/2 Q . U . Q)`` with a
|
|
10
|
+
diagonal Cartesian MSD tensor ``U = diag(u_par, u_par, u_perp)`` (Angstrom^2),
|
|
11
|
+
optionally a function of time. ``u_par``/``u_perp`` are fittable.
|
|
12
|
+
|
|
13
|
+
2. **MD-derived** (:func:`msd_tensor_from_frames`): read the *same* tensor
|
|
14
|
+
straight off an MD trajectory as the covariance of atomic displacements.
|
|
15
|
+
This closes the loop with :mod:`midas_2d.debye` -- the ensemble spread of
|
|
16
|
+
coordinates *is* the disorder, and this function recovers the effective ``U``
|
|
17
|
+
that an analytic DWF would need to reproduce it.
|
|
18
|
+
|
|
19
|
+
Convention: ``Q`` in 1/A, ``U`` in A^2, platelet normal = +z (the c axis).
|
|
20
|
+
``T`` multiplies the structure-factor *amplitude*; intensity carries ``T^2``.
|
|
21
|
+
"""
|
|
22
|
+
from __future__ import annotations
|
|
23
|
+
|
|
24
|
+
from typing import TYPE_CHECKING, Optional
|
|
25
|
+
|
|
26
|
+
if TYPE_CHECKING: # pragma: no cover
|
|
27
|
+
import torch
|
|
28
|
+
|
|
29
|
+
__all__ = [
|
|
30
|
+
"dwf_amplitude",
|
|
31
|
+
"AnisotropicMSD",
|
|
32
|
+
"TransientMSD",
|
|
33
|
+
"msd_tensor_from_frames",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def dwf_amplitude(q_vec, u_par, u_perp):
|
|
38
|
+
"""Anisotropic Debye-Waller *amplitude* factor ``exp(-1/2 Q.U.Q)``.
|
|
39
|
+
|
|
40
|
+
Parameters
|
|
41
|
+
----------
|
|
42
|
+
q_vec : tensor (..., 3)
|
|
43
|
+
Scattering vectors (1/A).
|
|
44
|
+
u_par, u_perp : float or tensor
|
|
45
|
+
In-plane (x, y) and out-of-plane (z) mean-square displacements (A^2).
|
|
46
|
+
|
|
47
|
+
Returns
|
|
48
|
+
-------
|
|
49
|
+
tensor (...)
|
|
50
|
+
The amplitude multiplier; square it for the intensity multiplier.
|
|
51
|
+
"""
|
|
52
|
+
import torch
|
|
53
|
+
q_vec = torch.as_tensor(q_vec)
|
|
54
|
+
u_par = torch.as_tensor(u_par, dtype=q_vec.dtype, device=q_vec.device)
|
|
55
|
+
u_perp = torch.as_tensor(u_perp, dtype=q_vec.dtype, device=q_vec.device)
|
|
56
|
+
qx, qy, qz = q_vec[..., 0], q_vec[..., 1], q_vec[..., 2]
|
|
57
|
+
quad = u_par * (qx * qx + qy * qy) + u_perp * (qz * qz)
|
|
58
|
+
return torch.exp(-0.5 * quad)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class AnisotropicMSD:
|
|
62
|
+
"""Fittable static anisotropic MSD tensor (positivity via softplus).
|
|
63
|
+
|
|
64
|
+
``u_par``/``u_perp`` are stored as unconstrained ``raw_*`` parameters; the
|
|
65
|
+
physical values are ``softplus(raw)`` so optimisation stays unconstrained.
|
|
66
|
+
"""
|
|
67
|
+
def __init__(self, u_par=1e-3, u_perp=1e-3, *, dtype=None, device=None,
|
|
68
|
+
requires_grad=True):
|
|
69
|
+
import torch
|
|
70
|
+
dtype = dtype or torch.float64
|
|
71
|
+
self.raw_par = _inv_softplus(torch.as_tensor(u_par, dtype=dtype, device=device))
|
|
72
|
+
self.raw_perp = _inv_softplus(torch.as_tensor(u_perp, dtype=dtype, device=device))
|
|
73
|
+
if requires_grad:
|
|
74
|
+
self.raw_par = self.raw_par.clone().detach().requires_grad_(True)
|
|
75
|
+
self.raw_perp = self.raw_perp.clone().detach().requires_grad_(True)
|
|
76
|
+
|
|
77
|
+
@property
|
|
78
|
+
def u_par(self):
|
|
79
|
+
import torch
|
|
80
|
+
return torch.nn.functional.softplus(self.raw_par)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def u_perp(self):
|
|
84
|
+
import torch
|
|
85
|
+
return torch.nn.functional.softplus(self.raw_perp)
|
|
86
|
+
|
|
87
|
+
def parameters(self):
|
|
88
|
+
return [self.raw_par, self.raw_perp]
|
|
89
|
+
|
|
90
|
+
def amplitude(self, q_vec):
|
|
91
|
+
return dwf_amplitude(q_vec, self.u_par, self.u_perp)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class TransientMSD:
|
|
95
|
+
"""Per-delay anisotropic MSD: ``u_par(t)``, ``u_perp(t)`` over T pump-probe
|
|
96
|
+
delays. Each delay has its own fittable (softplus-positive) pair, so a full
|
|
97
|
+
time series of patterns is inverted jointly with shared structure.
|
|
98
|
+
"""
|
|
99
|
+
def __init__(self, n_delays, u_par0=1e-3, u_perp0=1e-3, *, dtype=None,
|
|
100
|
+
device=None, requires_grad=True):
|
|
101
|
+
import torch
|
|
102
|
+
dtype = dtype or torch.float64
|
|
103
|
+
rp = _inv_softplus(torch.full((n_delays,), float(u_par0), dtype=dtype, device=device))
|
|
104
|
+
rq = _inv_softplus(torch.full((n_delays,), float(u_perp0), dtype=dtype, device=device))
|
|
105
|
+
if requires_grad:
|
|
106
|
+
rp = rp.clone().detach().requires_grad_(True)
|
|
107
|
+
rq = rq.clone().detach().requires_grad_(True)
|
|
108
|
+
self.raw_par = rp
|
|
109
|
+
self.raw_perp = rq
|
|
110
|
+
self.n_delays = int(n_delays)
|
|
111
|
+
|
|
112
|
+
@property
|
|
113
|
+
def u_par(self):
|
|
114
|
+
import torch
|
|
115
|
+
return torch.nn.functional.softplus(self.raw_par)
|
|
116
|
+
|
|
117
|
+
@property
|
|
118
|
+
def u_perp(self):
|
|
119
|
+
import torch
|
|
120
|
+
return torch.nn.functional.softplus(self.raw_perp)
|
|
121
|
+
|
|
122
|
+
def parameters(self):
|
|
123
|
+
return [self.raw_par, self.raw_perp]
|
|
124
|
+
|
|
125
|
+
def amplitude(self, q_vec, delay_index):
|
|
126
|
+
return dwf_amplitude(q_vec, self.u_par[delay_index], self.u_perp[delay_index])
|
|
127
|
+
|
|
128
|
+
def anisotropy(self):
|
|
129
|
+
"""``u_perp / u_par`` per delay -- the out-of-plane/in-plane ratio that
|
|
130
|
+
is the experimental disordering signature."""
|
|
131
|
+
return self.u_perp / self.u_par
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def msd_tensor_from_frames(frames, *, reference=None):
|
|
135
|
+
"""Effective MSD tensor ``U`` (A^2) from an MD trajectory.
|
|
136
|
+
|
|
137
|
+
``U = < (r - <r>) (r - <r>)^T >`` averaged over atoms and frames. The
|
|
138
|
+
diagonal entries are ``(u_xx, u_yy, u_zz)``; for a platelet,
|
|
139
|
+
``u_zz`` is ``u_perp`` and ``(u_xx + u_yy)/2`` is ``u_par``.
|
|
140
|
+
|
|
141
|
+
Parameters
|
|
142
|
+
----------
|
|
143
|
+
frames : tensor (F, M, 3)
|
|
144
|
+
reference : tensor (M, 3), optional
|
|
145
|
+
Mean positions; defaults to the per-atom time average.
|
|
146
|
+
|
|
147
|
+
Returns
|
|
148
|
+
-------
|
|
149
|
+
U : tensor (3, 3)
|
|
150
|
+
"""
|
|
151
|
+
import torch
|
|
152
|
+
frames = torch.as_tensor(frames)
|
|
153
|
+
if frames.dim() == 2:
|
|
154
|
+
frames = frames.unsqueeze(0)
|
|
155
|
+
ref = reference if reference is not None else frames.mean(dim=0) # (M,3)
|
|
156
|
+
disp = (frames - ref[None]).reshape(-1, 3) # (F*M, 3)
|
|
157
|
+
U = (disp.T @ disp) / disp.shape[0] # (3,3)
|
|
158
|
+
return U
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _inv_softplus(y):
|
|
162
|
+
"""Inverse of softplus so that softplus(raw) == y for the init value."""
|
|
163
|
+
import torch
|
|
164
|
+
y = torch.as_tensor(y)
|
|
165
|
+
# raw = log(exp(y) - 1); stable for small/large y.
|
|
166
|
+
return torch.where(y > 20, y, torch.log(torch.expm1(y.clamp(min=1e-12))))
|