midas-pdf 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 (109) hide show
  1. midas_pdf-0.1.0/LICENSE +53 -0
  2. midas_pdf-0.1.0/PKG-INFO +127 -0
  3. midas_pdf-0.1.0/README.md +93 -0
  4. midas_pdf-0.1.0/midas_pdf/__init__.py +118 -0
  5. midas_pdf-0.1.0/midas_pdf/aniso_refine.py +206 -0
  6. midas_pdf-0.1.0/midas_pdf/bayesian_refine.py +332 -0
  7. midas_pdf-0.1.0/midas_pdf/cif.py +445 -0
  8. midas_pdf-0.1.0/midas_pdf/cli/__init__.py +20 -0
  9. midas_pdf-0.1.0/midas_pdf/cli/_common.py +84 -0
  10. midas_pdf-0.1.0/midas_pdf/cli/cif_cmd.py +110 -0
  11. midas_pdf-0.1.0/midas_pdf/cli/coreshell_cmd.py +101 -0
  12. midas_pdf-0.1.0/midas_pdf/cli/joint_cmd.py +146 -0
  13. midas_pdf-0.1.0/midas_pdf/cli/multiphase_cmd.py +112 -0
  14. midas_pdf-0.1.0/midas_pdf/cli/refine_cmd.py +95 -0
  15. midas_pdf-0.1.0/midas_pdf/cli/rmc_cmd.py +174 -0
  16. midas_pdf-0.1.0/midas_pdf/composition.py +275 -0
  17. midas_pdf-0.1.0/midas_pdf/compton.py +133 -0
  18. midas_pdf-0.1.0/midas_pdf/conventions.py +126 -0
  19. midas_pdf-0.1.0/midas_pdf/corrections.py +360 -0
  20. midas_pdf-0.1.0/midas_pdf/cross_section.py +131 -0
  21. midas_pdf-0.1.0/midas_pdf/data/fluor_edges.json +1 -0
  22. midas_pdf-0.1.0/midas_pdf/data/incoherent_hubbell.json +1 -0
  23. midas_pdf-0.1.0/midas_pdf/deltapdf.py +215 -0
  24. midas_pdf-0.1.0/midas_pdf/fluorescence.py +123 -0
  25. midas_pdf-0.1.0/midas_pdf/frontend.py +107 -0
  26. midas_pdf-0.1.0/midas_pdf/gr.py +27 -0
  27. midas_pdf-0.1.0/midas_pdf/ionic_form_factors.py +188 -0
  28. midas_pdf-0.1.0/midas_pdf/model_comparison.py +219 -0
  29. midas_pdf-0.1.0/midas_pdf/ms.py +511 -0
  30. midas_pdf-0.1.0/midas_pdf/ms_transport.py +168 -0
  31. midas_pdf-0.1.0/midas_pdf/multi_phase.py +433 -0
  32. midas_pdf-0.1.0/midas_pdf/multiple_scattering.py +73 -0
  33. midas_pdf-0.1.0/midas_pdf/normalize.py +127 -0
  34. midas_pdf-0.1.0/midas_pdf/pipeline.py +62 -0
  35. midas_pdf-0.1.0/midas_pdf/placzek.py +61 -0
  36. midas_pdf-0.1.0/midas_pdf/refine.py +179 -0
  37. midas_pdf-0.1.0/midas_pdf/rmc/__init__.py +54 -0
  38. midas_pdf-0.1.0/midas_pdf/rmc/analysis.py +248 -0
  39. midas_pdf-0.1.0/midas_pdf/rmc/cluster_moves.py +199 -0
  40. midas_pdf-0.1.0/midas_pdf/rmc/driver.py +139 -0
  41. midas_pdf-0.1.0/midas_pdf/rmc/ensemble.py +195 -0
  42. midas_pdf-0.1.0/midas_pdf/rmc/gc_moves.py +194 -0
  43. midas_pdf-0.1.0/midas_pdf/rmc/histogram.py +100 -0
  44. midas_pdf-0.1.0/midas_pdf/rmc/moves.py +226 -0
  45. midas_pdf-0.1.0/midas_pdf/rmc/supercell.py +170 -0
  46. midas_pdf-0.1.0/midas_pdf/saxs/__init__.py +64 -0
  47. midas_pdf-0.1.0/midas_pdf/saxs/core_shell.py +122 -0
  48. midas_pdf-0.1.0/midas_pdf/saxs/form_factors.py +212 -0
  49. midas_pdf-0.1.0/midas_pdf/saxs/joint.py +205 -0
  50. midas_pdf-0.1.0/midas_pdf/saxs/joint_bayesian.py +334 -0
  51. midas_pdf-0.1.0/midas_pdf/saxs/joint_three_way.py +198 -0
  52. midas_pdf-0.1.0/midas_pdf/saxs/joint_three_way_bayesian.py +374 -0
  53. midas_pdf-0.1.0/midas_pdf/saxs/model.py +165 -0
  54. midas_pdf-0.1.0/midas_pdf/saxs/wide_band.py +265 -0
  55. midas_pdf-0.1.0/midas_pdf/strain_pdf.py +255 -0
  56. midas_pdf-0.1.0/midas_pdf/structure.py +391 -0
  57. midas_pdf-0.1.0/midas_pdf/validate.py +135 -0
  58. midas_pdf-0.1.0/midas_pdf.egg-info/PKG-INFO +127 -0
  59. midas_pdf-0.1.0/midas_pdf.egg-info/SOURCES.txt +107 -0
  60. midas_pdf-0.1.0/midas_pdf.egg-info/dependency_links.txt +1 -0
  61. midas_pdf-0.1.0/midas_pdf.egg-info/entry_points.txt +7 -0
  62. midas_pdf-0.1.0/midas_pdf.egg-info/requires.txt +12 -0
  63. midas_pdf-0.1.0/midas_pdf.egg-info/top_level.txt +1 -0
  64. midas_pdf-0.1.0/pyproject.toml +79 -0
  65. midas_pdf-0.1.0/setup.cfg +4 -0
  66. midas_pdf-0.1.0/tests/test_aniso_occupancy.py +196 -0
  67. midas_pdf-0.1.0/tests/test_bayesian_refine.py +118 -0
  68. midas_pdf-0.1.0/tests/test_bug_fixes_rev15.py +248 -0
  69. midas_pdf-0.1.0/tests/test_cif.py +414 -0
  70. midas_pdf-0.1.0/tests/test_cli.py +245 -0
  71. midas_pdf-0.1.0/tests/test_cli_multi_coreshell.py +195 -0
  72. midas_pdf-0.1.0/tests/test_composition.py +71 -0
  73. midas_pdf-0.1.0/tests/test_compton.py +56 -0
  74. midas_pdf-0.1.0/tests/test_conventions.py +52 -0
  75. midas_pdf-0.1.0/tests/test_corrections.py +138 -0
  76. midas_pdf-0.1.0/tests/test_cross_section.py +57 -0
  77. midas_pdf-0.1.0/tests/test_cylinder_ms.py +44 -0
  78. midas_pdf-0.1.0/tests/test_double_scattering.py +60 -0
  79. midas_pdf-0.1.0/tests/test_fluorescence.py +28 -0
  80. midas_pdf-0.1.0/tests/test_frontend.py +74 -0
  81. midas_pdf-0.1.0/tests/test_ionic_form_factors.py +149 -0
  82. midas_pdf-0.1.0/tests/test_model_comparison.py +146 -0
  83. midas_pdf-0.1.0/tests/test_ms.py +81 -0
  84. midas_pdf-0.1.0/tests/test_multi_phase.py +268 -0
  85. midas_pdf-0.1.0/tests/test_multiple_scattering.py +59 -0
  86. midas_pdf-0.1.0/tests/test_normalize.py +68 -0
  87. midas_pdf-0.1.0/tests/test_pipeline.py +72 -0
  88. midas_pdf-0.1.0/tests/test_refine.py +52 -0
  89. midas_pdf-0.1.0/tests/test_refine_background.py +53 -0
  90. midas_pdf-0.1.0/tests/test_rev4_additions.py +186 -0
  91. midas_pdf-0.1.0/tests/test_rmc_cluster.py +156 -0
  92. midas_pdf-0.1.0/tests/test_rmc_day1.py +192 -0
  93. midas_pdf-0.1.0/tests/test_rmc_day2.py +229 -0
  94. midas_pdf-0.1.0/tests/test_rmc_day3.py +158 -0
  95. midas_pdf-0.1.0/tests/test_rmc_day4.py +134 -0
  96. midas_pdf-0.1.0/tests/test_rmc_gc.py +181 -0
  97. midas_pdf-0.1.0/tests/test_saxs_core_shell.py +131 -0
  98. midas_pdf-0.1.0/tests/test_saxs_day1.py +167 -0
  99. midas_pdf-0.1.0/tests/test_saxs_joint.py +197 -0
  100. midas_pdf-0.1.0/tests/test_saxs_joint_bayesian.py +121 -0
  101. midas_pdf-0.1.0/tests/test_saxs_three_way.py +145 -0
  102. midas_pdf-0.1.0/tests/test_saxs_three_way_bayesian.py +149 -0
  103. midas_pdf-0.1.0/tests/test_saxs_wide_band.py +158 -0
  104. midas_pdf-0.1.0/tests/test_sequence_dpdf.py +152 -0
  105. midas_pdf-0.1.0/tests/test_strain_pdf.py +130 -0
  106. midas_pdf-0.1.0/tests/test_structure.py +78 -0
  107. midas_pdf-0.1.0/tests/test_transport_ms.py +67 -0
  108. midas_pdf-0.1.0/tests/test_uncertainty.py +48 -0
  109. midas_pdf-0.1.0/tests/test_validate.py +42 -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,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: midas-pdf
3
+ Version: 0.1.0
4
+ Summary: Differentiable, error-propagating total-scattering / pair-distribution-function (PDF, G(r)) pipeline. Thin Faber-Ziman layer over midas-hkls (form factors) and midas-integrate-v2 (integration, Compton, sine FT).
5
+ Author-email: Hemant Sharma <hsharma@anl.gov>
6
+ License-Expression: BSD-3-Clause
7
+ Project-URL: Homepage, https://github.com/marinerhemant/MIDAS
8
+ Project-URL: Issues, https://github.com/marinerhemant/MIDAS/issues
9
+ Keywords: MIDAS,X-ray,total scattering,pair distribution function,PDF,G(r),delta-PDF,Faber-Ziman,differentiable,PyTorch,uncertainty propagation
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Science/Research
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Topic :: Scientific/Engineering :: Physics
19
+ Classifier: Topic :: Scientific/Engineering :: Chemistry
20
+ Requires-Python: >=3.9
21
+ Description-Content-Type: text/markdown
22
+ License-File: LICENSE
23
+ Requires-Dist: numpy>=1.22
24
+ Requires-Dist: torch>=2.1
25
+ Requires-Dist: midas-hkls>=0.5.0
26
+ Requires-Dist: midas-integrate-v2>=0.1.0
27
+ Provides-Extra: dev
28
+ Requires-Dist: pytest>=7.0; extra == "dev"
29
+ Requires-Dist: pytest-cov>=4.0; extra == "dev"
30
+ Requires-Dist: matplotlib>=3.6; extra == "dev"
31
+ Provides-Extra: bayes
32
+ Requires-Dist: pyro-ppl>=1.9.0; extra == "bayes"
33
+ Dynamic: license-file
34
+
35
+ # midas-pdf
36
+
37
+ **Differentiable, error-propagating total-scattering / pair-distribution-function
38
+ (PDF, *G(r)*) pipeline.**
39
+
40
+ `midas-pdf` is a deliberately thin layer. Almost everything it needs already
41
+ exists elsewhere in MIDAS and is reused rather than reimplemented:
42
+
43
+ | Stage | Provided by | Status |
44
+ |-------|-------------|--------|
45
+ | detector geometry + wavelength (+ covariance) | `midas-calibrate-v2` | existing |
46
+ | pixels → I(Q) with σ (polygon-exact, pol/solid-angle/dark) | `midas-integrate-v2` | existing |
47
+ | atomic form factors f(Q), anomalous f′,f″ (differentiable) | `midas-hkls` | existing |
48
+ | Compton / incoherent subtraction | `midas-integrate-v2.corrections.compton` | existing |
49
+ | S(Q) → G(r) sine FT **with σ propagation** | `midas-integrate-v2.pdf` | existing |
50
+ | **polyatomic Faber-Ziman normalization** ⟨f²⟩, ⟨f⟩² | **`midas-pdf` (new)** | this package |
51
+ | **Δ-PDF (difference PDF) for time-resolved/operando** | **`midas-pdf` (new)** | this package |
52
+
53
+ The single piece that did not exist anywhere was the **composition layer**: the
54
+ existing `midas_integrate_v2.pdf.normalize_to_S` is *monoatomic* (it divides by a
55
+ single ⟨f²⟩). Real total scattering of a polyatomic sample needs the Faber-Ziman
56
+ form, which requires both ⟨f²⟩(Q) and ⟨f⟩²(Q) built from the sample composition.
57
+ That bridge — and the Δ-PDF helper — is all `midas-pdf` adds.
58
+
59
+ ## What is novel
60
+
61
+ Every arrow in the chain is a torch operation carrying a 1σ uncertainty, so the
62
+ pipeline is **end-to-end differentiable** *and* **end-to-end error-propagating** —
63
+ a combination no production total-scattering tool (PDFgetX3 / PDFgetN / GudrunX)
64
+ offers. Concretely this enables:
65
+
66
+ * gradient-based **normalization refinement** (`refine.py`): `scale`/`offset`/ρ₀
67
+ are fit by L-BFGS against model-free physics (⟨S⟩→1 at high Q, G(r)=−4πρ₀r at
68
+ low r) — the "ad hoc scale twiddling" of PDF analysis becomes an optimization;
69
+ * an analytic 1σ band on every G(r) point, **validated** against a Monte-Carlo
70
+ bootstrap to <1% (`dev/demo_sigma_validation.py`) — error propagation is the
71
+ one thing existing software tends to drop;
72
+ * statistically-meaningful **Δ-PDF** difference maps for time-resolved studies:
73
+ σ²(ΔG) = σ²(G₁) + σ²(G₂), so a feature change can be tested against noise;
74
+ * differentiability in atomic positions (`validate.py`, Debye equation), so the
75
+ same code is a forward model for structure refinement against G(r).
76
+
77
+ ## Modules
78
+
79
+ | Module | Contents |
80
+ |--------|----------|
81
+ | `composition.py` | `Composition` → ⟨f⟩(Q), ⟨f²⟩(Q), Laue term, Compton |
82
+ | `compton.py` | Hubbell tabulated incoherent scattering + Breit-Dirac recoil |
83
+ | `corrections.py` | Q-dependent detector efficiency, flat-plate self-absorption (MAC-backed) |
84
+ | `fluorescence.py` | `expected_fluorescence`: which elements fluoresce at a given energy |
85
+ | `multiple_scattering.py` | `lumped_background`: Tier-1 smooth MS/fluorescence/air background |
86
+ | `cross_section.py` | `differential_cross_section`: per-atom dσ/dΩ(Q) (MS engine) |
87
+ | `ms.py` | first-principles MS: analytic single + double scattering, Monte-Carlo references (slab + cylinder) |
88
+ | `ms_transport.py` | all-orders MS by differentiable discrete-ordinates radiative transfer (slab) |
89
+ | `structure.py` | differentiable small-box PDF (PDFfit-style) forward model + error-aware refinement |
90
+ | `normalize.py` | `faber_ziman_S`: I(Q) → S(Q) with σ (and lumped `background`) |
91
+
92
+ Runnable, one-per-capability demonstrations live in [`examples/`](examples/).
93
+ | `gr.py` | re-export of the reused sine FT (S(Q) → G(r) with σ) |
94
+ | `pipeline.py` | `i_of_q_to_Gr`: I(Q) → G(r) end to end |
95
+ | `frontend.py` | `image_to_iq`, `image_to_Gr`: detector pixels → G(r) |
96
+ | `conventions.py` | `structure_function_F`, `pair_distribution_g`, `total_correlation_T`, `radial_distribution_R` |
97
+ | `refine.py` | `refine_normalization`: differentiable scale/offset/ρ₀ fit |
98
+ | `deltapdf.py` | `delta_pdf`, `significant_mask`: difference PDF + n-σ test |
99
+ | `validate.py` | `debye_scattering_intensity`, `synthetic_powder_image`: model-free references |
100
+
101
+ ## Quick start
102
+
103
+ ```python
104
+ import torch
105
+ from midas_pdf import Composition, i_of_q_to_Gr
106
+
107
+ comp = Composition({"Si": 1, "O": 2}) # SiO2, number fractions
108
+ q = torch.linspace(0.5, 25.0, 2000, dtype=torch.float64)
109
+ r = torch.linspace(0.0, 10.0, 1000, dtype=torch.float64)
110
+
111
+ # I_q, sigma_I come from midas-integrate-v2 (pixels -> I(Q) with sigma)
112
+ G, sigma_G, S = i_of_q_to_Gr(
113
+ q, I_q, comp, r,
114
+ wavelength_A=0.1665, sigma_intensity=sigma_I,
115
+ compton=True, q_max=22.0,
116
+ )
117
+ ```
118
+
119
+ ## Conventions
120
+
121
+ Default is the **Faber-Ziman** total structure factor (X-ray, neutral-atom
122
+ form factors), matching the PDFgetX3 default. `S(Q) → 1` as `Q → ∞`;
123
+ `G(r) = (2/π) ∫ Q[S(Q)-1] sin(Qr) W(Q) dQ`. Window defaults to Lorch.
124
+ Convention choice (FZ vs Keen; which of S/F/G/g/D/T to report) is intended to be
125
+ settled with the experimental collaborators — see `dev/PLAN.md`.
126
+
127
+ See `dev/PLAN.md` for the phased build plan and open items.
@@ -0,0 +1,93 @@
1
+ # midas-pdf
2
+
3
+ **Differentiable, error-propagating total-scattering / pair-distribution-function
4
+ (PDF, *G(r)*) pipeline.**
5
+
6
+ `midas-pdf` is a deliberately thin layer. Almost everything it needs already
7
+ exists elsewhere in MIDAS and is reused rather than reimplemented:
8
+
9
+ | Stage | Provided by | Status |
10
+ |-------|-------------|--------|
11
+ | detector geometry + wavelength (+ covariance) | `midas-calibrate-v2` | existing |
12
+ | pixels → I(Q) with σ (polygon-exact, pol/solid-angle/dark) | `midas-integrate-v2` | existing |
13
+ | atomic form factors f(Q), anomalous f′,f″ (differentiable) | `midas-hkls` | existing |
14
+ | Compton / incoherent subtraction | `midas-integrate-v2.corrections.compton` | existing |
15
+ | S(Q) → G(r) sine FT **with σ propagation** | `midas-integrate-v2.pdf` | existing |
16
+ | **polyatomic Faber-Ziman normalization** ⟨f²⟩, ⟨f⟩² | **`midas-pdf` (new)** | this package |
17
+ | **Δ-PDF (difference PDF) for time-resolved/operando** | **`midas-pdf` (new)** | this package |
18
+
19
+ The single piece that did not exist anywhere was the **composition layer**: the
20
+ existing `midas_integrate_v2.pdf.normalize_to_S` is *monoatomic* (it divides by a
21
+ single ⟨f²⟩). Real total scattering of a polyatomic sample needs the Faber-Ziman
22
+ form, which requires both ⟨f²⟩(Q) and ⟨f⟩²(Q) built from the sample composition.
23
+ That bridge — and the Δ-PDF helper — is all `midas-pdf` adds.
24
+
25
+ ## What is novel
26
+
27
+ Every arrow in the chain is a torch operation carrying a 1σ uncertainty, so the
28
+ pipeline is **end-to-end differentiable** *and* **end-to-end error-propagating** —
29
+ a combination no production total-scattering tool (PDFgetX3 / PDFgetN / GudrunX)
30
+ offers. Concretely this enables:
31
+
32
+ * gradient-based **normalization refinement** (`refine.py`): `scale`/`offset`/ρ₀
33
+ are fit by L-BFGS against model-free physics (⟨S⟩→1 at high Q, G(r)=−4πρ₀r at
34
+ low r) — the "ad hoc scale twiddling" of PDF analysis becomes an optimization;
35
+ * an analytic 1σ band on every G(r) point, **validated** against a Monte-Carlo
36
+ bootstrap to <1% (`dev/demo_sigma_validation.py`) — error propagation is the
37
+ one thing existing software tends to drop;
38
+ * statistically-meaningful **Δ-PDF** difference maps for time-resolved studies:
39
+ σ²(ΔG) = σ²(G₁) + σ²(G₂), so a feature change can be tested against noise;
40
+ * differentiability in atomic positions (`validate.py`, Debye equation), so the
41
+ same code is a forward model for structure refinement against G(r).
42
+
43
+ ## Modules
44
+
45
+ | Module | Contents |
46
+ |--------|----------|
47
+ | `composition.py` | `Composition` → ⟨f⟩(Q), ⟨f²⟩(Q), Laue term, Compton |
48
+ | `compton.py` | Hubbell tabulated incoherent scattering + Breit-Dirac recoil |
49
+ | `corrections.py` | Q-dependent detector efficiency, flat-plate self-absorption (MAC-backed) |
50
+ | `fluorescence.py` | `expected_fluorescence`: which elements fluoresce at a given energy |
51
+ | `multiple_scattering.py` | `lumped_background`: Tier-1 smooth MS/fluorescence/air background |
52
+ | `cross_section.py` | `differential_cross_section`: per-atom dσ/dΩ(Q) (MS engine) |
53
+ | `ms.py` | first-principles MS: analytic single + double scattering, Monte-Carlo references (slab + cylinder) |
54
+ | `ms_transport.py` | all-orders MS by differentiable discrete-ordinates radiative transfer (slab) |
55
+ | `structure.py` | differentiable small-box PDF (PDFfit-style) forward model + error-aware refinement |
56
+ | `normalize.py` | `faber_ziman_S`: I(Q) → S(Q) with σ (and lumped `background`) |
57
+
58
+ Runnable, one-per-capability demonstrations live in [`examples/`](examples/).
59
+ | `gr.py` | re-export of the reused sine FT (S(Q) → G(r) with σ) |
60
+ | `pipeline.py` | `i_of_q_to_Gr`: I(Q) → G(r) end to end |
61
+ | `frontend.py` | `image_to_iq`, `image_to_Gr`: detector pixels → G(r) |
62
+ | `conventions.py` | `structure_function_F`, `pair_distribution_g`, `total_correlation_T`, `radial_distribution_R` |
63
+ | `refine.py` | `refine_normalization`: differentiable scale/offset/ρ₀ fit |
64
+ | `deltapdf.py` | `delta_pdf`, `significant_mask`: difference PDF + n-σ test |
65
+ | `validate.py` | `debye_scattering_intensity`, `synthetic_powder_image`: model-free references |
66
+
67
+ ## Quick start
68
+
69
+ ```python
70
+ import torch
71
+ from midas_pdf import Composition, i_of_q_to_Gr
72
+
73
+ comp = Composition({"Si": 1, "O": 2}) # SiO2, number fractions
74
+ q = torch.linspace(0.5, 25.0, 2000, dtype=torch.float64)
75
+ r = torch.linspace(0.0, 10.0, 1000, dtype=torch.float64)
76
+
77
+ # I_q, sigma_I come from midas-integrate-v2 (pixels -> I(Q) with sigma)
78
+ G, sigma_G, S = i_of_q_to_Gr(
79
+ q, I_q, comp, r,
80
+ wavelength_A=0.1665, sigma_intensity=sigma_I,
81
+ compton=True, q_max=22.0,
82
+ )
83
+ ```
84
+
85
+ ## Conventions
86
+
87
+ Default is the **Faber-Ziman** total structure factor (X-ray, neutral-atom
88
+ form factors), matching the PDFgetX3 default. `S(Q) → 1` as `Q → ∞`;
89
+ `G(r) = (2/π) ∫ Q[S(Q)-1] sin(Qr) W(Q) dQ`. Window defaults to Lorch.
90
+ Convention choice (FZ vs Keen; which of S/F/G/g/D/T to report) is intended to be
91
+ settled with the experimental collaborators — see `dev/PLAN.md`.
92
+
93
+ See `dev/PLAN.md` for the phased build plan and open items.
@@ -0,0 +1,118 @@
1
+ """midas-pdf — differentiable, error-propagating total-scattering / G(r) pipeline.
2
+
3
+ Thin Faber-Ziman normalization layer over ``midas-hkls`` (form factors) and
4
+ ``midas-integrate-v2`` (integration, Compton, sine FT with σ propagation).
5
+
6
+ Typical use::
7
+
8
+ from midas_pdf import Composition, i_of_q_to_Gr, delta_pdf
9
+ comp = Composition({"Si": 1, "O": 2})
10
+ G, sigma_G, S = i_of_q_to_Gr(q, I_q, comp, r, wavelength_A=0.1665,
11
+ sigma_intensity=sigma_I, q_max=22.0)
12
+ """
13
+ # macOS (and some Linux setups) can hit an OpenMP-duplicate abort at import
14
+ # time because torch and numpy each ship their own libomp copy. We set the
15
+ # env var *before* torch loads so users get a clean import instead of a
16
+ # cryptic runtime crash on their first line. Users who prefer to control
17
+ # it themselves can already have set it, in which case setdefault is a no-op.
18
+ import os as _os
19
+ _os.environ.setdefault("KMP_DUPLICATE_LIB_OK", "TRUE")
20
+
21
+ from .composition import Composition
22
+ from .compton import breit_dirac_factor, incoherent_scattering
23
+ from .conventions import (
24
+ pair_distribution_g,
25
+ radial_distribution_R,
26
+ structure_function_F,
27
+ total_correlation_T,
28
+ )
29
+ from .corrections import (
30
+ apply_detector_efficiency,
31
+ detector_efficiency,
32
+ flat_plate_transmission,
33
+ linear_attenuation_um,
34
+ )
35
+ from .deltapdf import delta_pdf, significant_mask
36
+ from .fluorescence import expected_fluorescence
37
+ from .frontend import image_to_Gr, image_to_iq
38
+ from .gr import G_of_r, R_px_to_Q, estimate_background, fourier_sine_transform
39
+ from .cross_section import differential_cross_section, total_cross_section
40
+ from .ms import (
41
+ CYLINDER_SLAB_FACTOR,
42
+ cylinder_effective_tau,
43
+ ms_background_on_grid,
44
+ multiple_scattering_mc,
45
+ multiple_scattering_mc_cylinder,
46
+ slab_double_scattering,
47
+ slab_optical_params,
48
+ slab_single_scattering_factor,
49
+ )
50
+ from .ms_transport import slab_transport_ms
51
+ from .multiple_scattering import lumped_background, polynomial_basis
52
+ from .structure import build_pair_list, pdffit_gr, refine_structure
53
+ from .normalize import faber_ziman_S
54
+ from .pipeline import i_of_q_to_Gr
55
+ from .refine import RefineResult, refine_normalization
56
+ from .validate import (
57
+ debye_scattering_intensity,
58
+ interatomic_distances,
59
+ synthetic_powder_image,
60
+ )
61
+
62
+ __version__ = "0.1.0"
63
+
64
+ __all__ = [
65
+ # core
66
+ "Composition",
67
+ "faber_ziman_S",
68
+ "i_of_q_to_Gr",
69
+ "fourier_sine_transform",
70
+ "G_of_r",
71
+ "R_px_to_Q",
72
+ "estimate_background",
73
+ # front-end (pixels → G(r))
74
+ "image_to_iq",
75
+ "image_to_Gr",
76
+ # convention / output-function family
77
+ "structure_function_F",
78
+ "pair_distribution_g",
79
+ "total_correlation_T",
80
+ "radial_distribution_R",
81
+ # corrections (Hubbell Compton, detector efficiency, absorption, fluorescence)
82
+ "incoherent_scattering",
83
+ "breit_dirac_factor",
84
+ "detector_efficiency",
85
+ "apply_detector_efficiency",
86
+ "flat_plate_transmission",
87
+ "linear_attenuation_um",
88
+ "expected_fluorescence",
89
+ # multiple scattering (Tier-1 lumped smooth background)
90
+ "lumped_background",
91
+ "polynomial_basis",
92
+ # multiple scattering (Tier-2/3: cross-section + first-principles estimators)
93
+ "differential_cross_section",
94
+ "total_cross_section",
95
+ "slab_single_scattering_factor",
96
+ "slab_optical_params",
97
+ "multiple_scattering_mc",
98
+ "slab_double_scattering",
99
+ "slab_transport_ms",
100
+ "multiple_scattering_mc_cylinder",
101
+ "cylinder_effective_tau",
102
+ "CYLINDER_SLAB_FACTOR",
103
+ "ms_background_on_grid",
104
+ # differentiable refinement
105
+ "refine_normalization",
106
+ "RefineResult",
107
+ # small-box structure modelling (PDFfit-style, error-aware)
108
+ "pdffit_gr",
109
+ "build_pair_list",
110
+ "refine_structure",
111
+ # Δ-PDF
112
+ "delta_pdf",
113
+ "significant_mask",
114
+ # model-free references / forward model
115
+ "debye_scattering_intensity",
116
+ "interatomic_distances",
117
+ "synthetic_powder_image",
118
+ ]
@@ -0,0 +1,206 @@
1
+ """Rev-15 anisotropic-ADP + partial-occupancy PDF refinement.
2
+
3
+ The Rev-8 :func:`midas_pdf.structure.refine_structure` refines a single
4
+ isotropic ``u_iso`` and full occupancy on every site. Real fits of low-
5
+ symmetry crystals, oxide superlattices, and solid solutions need:
6
+
7
+ * per-site **anisotropic** displacement tensors ``U_ij`` (3×3 symmetric,
8
+ 6 unique entries), and
9
+ * per-site **partial occupancy** (defect-vacancy / solid-solution work).
10
+
11
+ This module adds a differentiable L-BFGS refiner that co-refines those two
12
+ sets of DOFs together with the cubic-lattice constant and PDF scale, and
13
+ reports Hessian-derived 1σ on every DOF. It preserves the diagonal
14
+ constraint on ``U`` (only the six unique entries — ``U11 U22 U33 U12 U13 U23``
15
+ — are stored and refined; the tensor is reconstructed symmetrically each
16
+ forward pass). A positive-definiteness soft penalty keeps the fit
17
+ physical without needing a hard constraint.
18
+ """
19
+ from __future__ import annotations
20
+
21
+ from typing import Optional
22
+
23
+ import numpy as np
24
+ import torch
25
+
26
+ from .structure import PairList, pdffit_gr
27
+
28
+
29
+ __all__ = [
30
+ "u_vector_to_matrix",
31
+ "u_matrix_to_vector",
32
+ "refine_aniso_occupancy",
33
+ "AnisoRefineResult",
34
+ ]
35
+
36
+
37
+ def u_vector_to_matrix(u_vec: torch.Tensor) -> torch.Tensor:
38
+ """Expand (n_uc, 6) unique entries [U11 U22 U33 U12 U13 U23] to
39
+ (n_uc, 3, 3) symmetric tensors, differentiable."""
40
+ u = torch.as_tensor(u_vec, dtype=torch.float64)
41
+ if u.ndim != 2 or u.shape[1] != 6:
42
+ raise ValueError(f"u_vec must be (n_uc, 6), got {tuple(u.shape)}")
43
+ n = u.shape[0]
44
+ U = torch.zeros((n, 3, 3), dtype=torch.float64)
45
+ U[:, 0, 0] = u[:, 0]
46
+ U[:, 1, 1] = u[:, 1]
47
+ U[:, 2, 2] = u[:, 2]
48
+ U[:, 0, 1] = u[:, 3]; U[:, 1, 0] = u[:, 3]
49
+ U[:, 0, 2] = u[:, 4]; U[:, 2, 0] = u[:, 4]
50
+ U[:, 1, 2] = u[:, 5]; U[:, 2, 1] = u[:, 5]
51
+ return U
52
+
53
+
54
+ def u_matrix_to_vector(U: torch.Tensor) -> torch.Tensor:
55
+ """Pack a (n_uc, 3, 3) symmetric tensor into (n_uc, 6) unique entries."""
56
+ U = torch.as_tensor(U, dtype=torch.float64)
57
+ if U.ndim != 3 or U.shape[1:] != (3, 3):
58
+ raise ValueError(f"U must be (n_uc, 3, 3), got {tuple(U.shape)}")
59
+ return torch.stack([U[:, 0, 0], U[:, 1, 1], U[:, 2, 2],
60
+ U[:, 0, 1], U[:, 0, 2], U[:, 1, 2]], dim=1)
61
+
62
+
63
+ class AnisoRefineResult(dict):
64
+ def __getattr__(self, name):
65
+ try: return self[name]
66
+ except KeyError: raise AttributeError(name) from None
67
+
68
+
69
+ def refine_aniso_occupancy(
70
+ crystal_tensor,
71
+ r: torch.Tensor | np.ndarray,
72
+ G_obs: torch.Tensor | np.ndarray,
73
+ pairs: PairList,
74
+ *,
75
+ sigma_obs: Optional[torch.Tensor | np.ndarray] = None,
76
+ init_a: Optional[float] = None,
77
+ init_u_iso: float = 0.006,
78
+ init_scale: float = 1.0,
79
+ refine_aniso: bool = True,
80
+ refine_occupancy: bool = False,
81
+ steps: int = 200,
82
+ lr: float = 0.05,
83
+ positive_definite_penalty: float = 1e3,
84
+ ) -> AnisoRefineResult:
85
+ """Co-refine cubic-``a``, ``scale``, per-site aniso ADPs and (optional)
86
+ per-site occupancy against ``G_obs``.
87
+
88
+ Parameter layout is:
89
+
90
+ θ = [a, scale] + (6 * n_uc aniso entries if refine_aniso else 0)
91
+ + (n_uc occupancies if refine_occupancy else 0)
92
+
93
+ On ``refine_aniso=False`` the fit still uses ``u_iso`` on every site so
94
+ the interface stays uniform. The positive-definite penalty adds
95
+ ``λ · sum(relu(-eigvals(U)))²`` to χ² — cheap, differentiable, and
96
+ it turns off automatically once every U is p.d.
97
+ """
98
+ r_t = torch.as_tensor(r, dtype=torch.float64)
99
+ G_t = torch.as_tensor(G_obs, dtype=torch.float64)
100
+ w = (torch.ones_like(G_t) if sigma_obs is None
101
+ else 1.0 / torch.as_tensor(sigma_obs, dtype=torch.float64).clamp(min=1e-12) ** 2)
102
+
103
+ lat0 = crystal_tensor.lattice_params.detach()
104
+ angles = lat0[3:]
105
+ a0 = float(lat0[0]) if init_a is None else float(init_a)
106
+ n_uc = pairs.n_uc
107
+
108
+ theta_parts = [torch.tensor([a0, init_scale], dtype=torch.float64)]
109
+ n_aniso = 6 * n_uc if refine_aniso else 0
110
+ if refine_aniso:
111
+ u_init = torch.tensor([init_u_iso, init_u_iso, init_u_iso, 0.0, 0.0, 0.0],
112
+ dtype=torch.float64)
113
+ theta_parts.append(u_init.repeat(n_uc))
114
+ n_occ = n_uc if refine_occupancy else 0
115
+ if refine_occupancy:
116
+ theta_parts.append(torch.ones(n_uc, dtype=torch.float64))
117
+ theta = torch.cat(theta_parts).clone().requires_grad_(True)
118
+
119
+ def unpack(th):
120
+ a = th[0]
121
+ scale = th[1]
122
+ idx = 2
123
+ u_aniso = None
124
+ occupancy = None
125
+ if refine_aniso:
126
+ u_flat = th[idx:idx + n_aniso].reshape(n_uc, 6)
127
+ u_aniso = u_vector_to_matrix(u_flat)
128
+ idx += n_aniso
129
+ if refine_occupancy:
130
+ occupancy = th[idx:idx + n_occ].clamp(min=0.0, max=1.0)
131
+ idx += n_occ
132
+ return a, scale, u_aniso, occupancy
133
+
134
+ def model(th):
135
+ a, scale, u_aniso, occupancy = unpack(th)
136
+ lp = torch.cat([a.reshape(1).expand(3), angles])
137
+ kwargs = {"lattice_params": lp, "scale": scale}
138
+ if u_aniso is not None:
139
+ kwargs["u_aniso"] = u_aniso
140
+ else:
141
+ kwargs["u_iso"] = init_u_iso
142
+ if occupancy is not None:
143
+ kwargs["occupancy"] = occupancy
144
+ return pdffit_gr(crystal_tensor, r_t, pairs, **kwargs)
145
+
146
+ def chi2(th):
147
+ res = G_t - model(th)
148
+ loss = (w * res * res).sum()
149
+ if refine_aniso and positive_definite_penalty > 0.0:
150
+ _, _, u_aniso, _ = unpack(th)
151
+ eigs = torch.linalg.eigvalsh(u_aniso) # (n_uc, 3)
152
+ neg = torch.relu(-eigs)
153
+ loss = loss + positive_definite_penalty * (neg * neg).sum()
154
+ return loss
155
+
156
+ opt = torch.optim.LBFGS([theta], lr=lr, max_iter=steps,
157
+ line_search_fn="strong_wolfe")
158
+ history: list[float] = []
159
+
160
+ def closure():
161
+ opt.zero_grad()
162
+ loss = chi2(theta)
163
+ loss.backward()
164
+ history.append(float(loss.detach()))
165
+ return loss
166
+
167
+ opt.step(closure)
168
+
169
+ with torch.no_grad():
170
+ th_det = theta.detach()
171
+ a_val, scale_val, u_aniso_val, occ_val = unpack(th_det)
172
+ G_fit = model(th_det)
173
+ ndof = max(int(G_t.numel()) - theta.numel(), 1)
174
+ chi2_red = float((w * (G_t - G_fit) ** 2).sum()) / ndof
175
+
176
+ # Hessian uncertainties (may be huge for weakly-identified aniso entries).
177
+ uncert = {}
178
+ try:
179
+ th_leaf = theta.detach().clone().requires_grad_(True)
180
+ H = torch.autograd.functional.hessian(chi2, th_leaf)
181
+ cov = 2.0 * torch.linalg.pinv(H)
182
+ sig = torch.sqrt(torch.diagonal(cov).clamp(min=0.0))
183
+ uncert["a"] = float(sig[0])
184
+ uncert["scale"] = float(sig[1])
185
+ idx = 2
186
+ if refine_aniso:
187
+ uncert["u_aniso"] = sig[idx:idx + n_aniso].reshape(n_uc, 6).tolist()
188
+ idx += n_aniso
189
+ if refine_occupancy:
190
+ uncert["occupancy"] = sig[idx:idx + n_occ].tolist()
191
+ except Exception:
192
+ uncert = {"error": "Hessian failed"}
193
+
194
+ return AnisoRefineResult(
195
+ fitted={
196
+ "a": float(a_val),
197
+ "scale": float(scale_val),
198
+ "u_aniso": u_aniso_val.tolist() if u_aniso_val is not None else None,
199
+ "occupancy": occ_val.tolist() if occ_val is not None else None,
200
+ },
201
+ uncertainty=uncert,
202
+ G_calc=G_fit.detach(),
203
+ chi2_reduced=chi2_red,
204
+ loss=history[-1] if history else float("nan"),
205
+ history=history, loss_history=history,
206
+ )