otter-hed 0.2.1__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.
- otter/__init__.py +45 -0
- otter/_version.py +8 -0
- otter/data/__init__.py +13 -0
- otter/data/elements.py +233 -0
- otter/data/helpers.py +131 -0
- otter/electronic/__init__.py +31 -0
- otter/electronic/continuum/__init__.py +32 -0
- otter/electronic/continuum/hybrid.py +124 -0
- otter/electronic/continuum/ideal.py +125 -0
- otter/electronic/continuum/interface.py +59 -0
- otter/electronic/continuum/scattering.py +4743 -0
- otter/electronic/continuum/tail.py +1630 -0
- otter/electronic/densities.py +298 -0
- otter/electronic/full_external.py +4933 -0
- otter/electronic/ks_dft.py +6620 -0
- otter/electronic/mixture.py +3104 -0
- otter/electronic/potential.py +380 -0
- otter/electronic/solvers/__init__.py +10 -0
- otter/electronic/solvers/bound.py +1053 -0
- otter/electronic/solvers/free.py +272 -0
- otter/electronic/thomas_fermi.py +1344 -0
- otter/electronic/xc.py +712 -0
- otter/experimental/__init__.py +17 -0
- otter/experimental/sc_feedback.py +544 -0
- otter/io/__init__.py +24 -0
- otter/io/_npz.py +43 -0
- otter/io/results.py +366 -0
- otter/io/state.py +1111 -0
- otter/ionic/__init__.py +100 -0
- otter/ionic/correlation.py +93 -0
- otter/ionic/lfc.py +762 -0
- otter/ionic/qoz.py +2074 -0
- otter/ionic/response.py +49 -0
- otter/literature.bib +523 -0
- otter/literature.py +459 -0
- otter/numerics/__init__.py +37 -0
- otter/numerics/constants.py +22 -0
- otter/numerics/grids.py +204 -0
- otter/numerics/interpolation.py +81 -0
- otter/numerics/transforms.py +168 -0
- otter/plotting.py +481 -0
- otter/workflows.py +1716 -0
- otter_hed-0.2.1.dist-info/METADATA +231 -0
- otter_hed-0.2.1.dist-info/RECORD +48 -0
- otter_hed-0.2.1.dist-info/WHEEL +5 -0
- otter_hed-0.2.1.dist-info/licenses/LICENSE +28 -0
- otter_hed-0.2.1.dist-info/licenses/THIRD_PARTY_NOTICES.md +68 -0
- otter_hed-0.2.1.dist-info/top_level.txt +1 -0
otter/__init__.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"""Public API for otter."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from otter._version import __version__
|
|
6
|
+
from otter.workflows import (
|
|
7
|
+
PlasmaWorkflowConfig,
|
|
8
|
+
continue_plasma_workflow_from_electronic_result,
|
|
9
|
+
parse_formula_composition,
|
|
10
|
+
resolve_plasma_composition,
|
|
11
|
+
run_formula_workflow,
|
|
12
|
+
solve_plasma_workflow,
|
|
13
|
+
)
|
|
14
|
+
from otter.io.state import (
|
|
15
|
+
StateExportOptions,
|
|
16
|
+
load_plasma_state,
|
|
17
|
+
save_plasma_state,
|
|
18
|
+
)
|
|
19
|
+
from otter.literature import (
|
|
20
|
+
CitationMixin,
|
|
21
|
+
bibliography_entries,
|
|
22
|
+
get_bibtex_ref_string,
|
|
23
|
+
get_cite_ref_string,
|
|
24
|
+
get_formatted_ref_string,
|
|
25
|
+
write_citations_markdown,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"__version__",
|
|
30
|
+
"CitationMixin",
|
|
31
|
+
"PlasmaWorkflowConfig",
|
|
32
|
+
"StateExportOptions",
|
|
33
|
+
"continue_plasma_workflow_from_electronic_result",
|
|
34
|
+
"bibliography_entries",
|
|
35
|
+
"get_bibtex_ref_string",
|
|
36
|
+
"get_cite_ref_string",
|
|
37
|
+
"get_formatted_ref_string",
|
|
38
|
+
"parse_formula_composition",
|
|
39
|
+
"load_plasma_state",
|
|
40
|
+
"resolve_plasma_composition",
|
|
41
|
+
"run_formula_workflow",
|
|
42
|
+
"save_plasma_state",
|
|
43
|
+
"solve_plasma_workflow",
|
|
44
|
+
"write_citations_markdown",
|
|
45
|
+
]
|
otter/_version.py
ADDED
otter/data/__init__.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""Chemical element data and density helpers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from otter.data.elements import Element, atomic_weight, element
|
|
5
|
+
from otter.data.helpers import ion_density_bohr3, mu_guess_from_density
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Element",
|
|
9
|
+
"atomic_weight",
|
|
10
|
+
"element",
|
|
11
|
+
"ion_density_bohr3",
|
|
12
|
+
"mu_guess_from_density",
|
|
13
|
+
]
|
otter/data/elements.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
"""
|
|
2
|
+
otter/data/elements.py
|
|
3
|
+
|
|
4
|
+
Periodic-table lookup utilities used by tests and example scripts.
|
|
5
|
+
|
|
6
|
+
Design goals
|
|
7
|
+
------------
|
|
8
|
+
- Keep one canonical mapping: atomic number (Z), symbol, atomic mass.
|
|
9
|
+
- Allow case-insensitive symbol lookup.
|
|
10
|
+
- Provide simple helpers for converting between Z/symbol/mass.
|
|
11
|
+
|
|
12
|
+
Notes
|
|
13
|
+
-----
|
|
14
|
+
Atomic masses are standard atomic weights for stable elements and conventional
|
|
15
|
+
representative values for short-lived/transuranic elements.
|
|
16
|
+
"""
|
|
17
|
+
from __future__ import annotations
|
|
18
|
+
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from typing import Dict, Iterable, Tuple
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass(frozen=True)
|
|
24
|
+
class Element:
|
|
25
|
+
"""Periodic-table entry."""
|
|
26
|
+
|
|
27
|
+
z: int
|
|
28
|
+
symbol: str
|
|
29
|
+
atomic_mass: float
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
_ELEMENT_ROWS: Tuple[Tuple[int, str, float], ...] = (
|
|
33
|
+
(1, "H", 1.008),
|
|
34
|
+
(2, "He", 4.002602),
|
|
35
|
+
(3, "Li", 6.94),
|
|
36
|
+
(4, "Be", 9.0121831),
|
|
37
|
+
(5, "B", 10.81),
|
|
38
|
+
(6, "C", 12.011),
|
|
39
|
+
(7, "N", 14.007),
|
|
40
|
+
(8, "O", 15.999),
|
|
41
|
+
(9, "F", 18.998403163),
|
|
42
|
+
(10, "Ne", 20.1797),
|
|
43
|
+
(11, "Na", 22.98976928),
|
|
44
|
+
(12, "Mg", 24.305),
|
|
45
|
+
(13, "Al", 26.9815385),
|
|
46
|
+
(14, "Si", 28.085),
|
|
47
|
+
(15, "P", 30.973761998),
|
|
48
|
+
(16, "S", 32.06),
|
|
49
|
+
(17, "Cl", 35.45),
|
|
50
|
+
(18, "Ar", 39.948),
|
|
51
|
+
(19, "K", 39.0983),
|
|
52
|
+
(20, "Ca", 40.078),
|
|
53
|
+
(21, "Sc", 44.955908),
|
|
54
|
+
(22, "Ti", 47.867),
|
|
55
|
+
(23, "V", 50.9415),
|
|
56
|
+
(24, "Cr", 51.9961),
|
|
57
|
+
(25, "Mn", 54.938044),
|
|
58
|
+
(26, "Fe", 55.845),
|
|
59
|
+
(27, "Co", 58.933194),
|
|
60
|
+
(28, "Ni", 58.6934),
|
|
61
|
+
(29, "Cu", 63.546),
|
|
62
|
+
(30, "Zn", 65.38),
|
|
63
|
+
(31, "Ga", 69.723),
|
|
64
|
+
(32, "Ge", 72.63),
|
|
65
|
+
(33, "As", 74.921595),
|
|
66
|
+
(34, "Se", 78.971),
|
|
67
|
+
(35, "Br", 79.904),
|
|
68
|
+
(36, "Kr", 83.798),
|
|
69
|
+
(37, "Rb", 85.4678),
|
|
70
|
+
(38, "Sr", 87.62),
|
|
71
|
+
(39, "Y", 88.90584),
|
|
72
|
+
(40, "Zr", 91.224),
|
|
73
|
+
(41, "Nb", 92.90637),
|
|
74
|
+
(42, "Mo", 95.95),
|
|
75
|
+
(43, "Tc", 98.0),
|
|
76
|
+
(44, "Ru", 101.07),
|
|
77
|
+
(45, "Rh", 102.90550),
|
|
78
|
+
(46, "Pd", 106.42),
|
|
79
|
+
(47, "Ag", 107.8682),
|
|
80
|
+
(48, "Cd", 112.414),
|
|
81
|
+
(49, "In", 114.818),
|
|
82
|
+
(50, "Sn", 118.710),
|
|
83
|
+
(51, "Sb", 121.760),
|
|
84
|
+
(52, "Te", 127.60),
|
|
85
|
+
(53, "I", 126.90447),
|
|
86
|
+
(54, "Xe", 131.293),
|
|
87
|
+
(55, "Cs", 132.90545196),
|
|
88
|
+
(56, "Ba", 137.327),
|
|
89
|
+
(57, "La", 138.90547),
|
|
90
|
+
(58, "Ce", 140.116),
|
|
91
|
+
(59, "Pr", 140.90766),
|
|
92
|
+
(60, "Nd", 144.242),
|
|
93
|
+
(61, "Pm", 145.0),
|
|
94
|
+
(62, "Sm", 150.36),
|
|
95
|
+
(63, "Eu", 151.964),
|
|
96
|
+
(64, "Gd", 157.25),
|
|
97
|
+
(65, "Tb", 158.92535),
|
|
98
|
+
(66, "Dy", 162.500),
|
|
99
|
+
(67, "Ho", 164.93033),
|
|
100
|
+
(68, "Er", 167.259),
|
|
101
|
+
(69, "Tm", 168.93422),
|
|
102
|
+
(70, "Yb", 173.045),
|
|
103
|
+
(71, "Lu", 174.9668),
|
|
104
|
+
(72, "Hf", 178.49),
|
|
105
|
+
(73, "Ta", 180.94788),
|
|
106
|
+
(74, "W", 183.84),
|
|
107
|
+
(75, "Re", 186.207),
|
|
108
|
+
(76, "Os", 190.23),
|
|
109
|
+
(77, "Ir", 192.217),
|
|
110
|
+
(78, "Pt", 195.084),
|
|
111
|
+
(79, "Au", 196.966569),
|
|
112
|
+
(80, "Hg", 200.592),
|
|
113
|
+
(81, "Tl", 204.38),
|
|
114
|
+
(82, "Pb", 207.2),
|
|
115
|
+
(83, "Bi", 208.98040),
|
|
116
|
+
(84, "Po", 209.0),
|
|
117
|
+
(85, "At", 210.0),
|
|
118
|
+
(86, "Rn", 222.0),
|
|
119
|
+
(87, "Fr", 223.0),
|
|
120
|
+
(88, "Ra", 226.0),
|
|
121
|
+
(89, "Ac", 227.0),
|
|
122
|
+
(90, "Th", 232.0377),
|
|
123
|
+
(91, "Pa", 231.03588),
|
|
124
|
+
(92, "U", 238.02891),
|
|
125
|
+
(93, "Np", 237.0),
|
|
126
|
+
(94, "Pu", 244.0),
|
|
127
|
+
(95, "Am", 243.0),
|
|
128
|
+
(96, "Cm", 247.0),
|
|
129
|
+
(97, "Bk", 247.0),
|
|
130
|
+
(98, "Cf", 251.0),
|
|
131
|
+
(99, "Es", 252.0),
|
|
132
|
+
(100, "Fm", 257.0),
|
|
133
|
+
(101, "Md", 258.0),
|
|
134
|
+
(102, "No", 259.0),
|
|
135
|
+
(103, "Lr", 266.0),
|
|
136
|
+
(104, "Rf", 267.0),
|
|
137
|
+
(105, "Db", 268.0),
|
|
138
|
+
(106, "Sg", 269.0),
|
|
139
|
+
(107, "Bh", 270.0),
|
|
140
|
+
(108, "Hs", 277.0),
|
|
141
|
+
(109, "Mt", 278.0),
|
|
142
|
+
(110, "Ds", 281.0),
|
|
143
|
+
(111, "Rg", 282.0),
|
|
144
|
+
(112, "Cn", 285.0),
|
|
145
|
+
(113, "Nh", 286.0),
|
|
146
|
+
(114, "Fl", 289.0),
|
|
147
|
+
(115, "Mc", 290.0),
|
|
148
|
+
(116, "Lv", 293.0),
|
|
149
|
+
(117, "Ts", 294.0),
|
|
150
|
+
(118, "Og", 294.0),
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
_BY_Z: Dict[int, Element] = {z: Element(z=z, symbol=symbol, atomic_mass=mass) for z, symbol, mass in _ELEMENT_ROWS}
|
|
154
|
+
_BY_SYMBOL: Dict[str, Element] = {item.symbol.upper(): item for item in _BY_Z.values()}
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _normalize_symbol(symbol: str) -> str:
|
|
158
|
+
"""Return canonical case-insensitive symbol key."""
|
|
159
|
+
s = str(symbol).strip()
|
|
160
|
+
if not s:
|
|
161
|
+
raise ValueError("Element symbol cannot be empty.")
|
|
162
|
+
return s.upper()
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def all_elements() -> Tuple[Element, ...]:
|
|
166
|
+
"""Return all element entries ordered by atomic number."""
|
|
167
|
+
return tuple(_BY_Z[z] for z in sorted(_BY_Z))
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def element_from_z(z: int) -> Element:
|
|
171
|
+
"""Return Element for atomic number z."""
|
|
172
|
+
z_int = int(z)
|
|
173
|
+
if z_int not in _BY_Z:
|
|
174
|
+
raise KeyError(f"Unknown atomic number: {z_int}")
|
|
175
|
+
return _BY_Z[z_int]
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
def element_from_symbol(symbol: str) -> Element:
|
|
179
|
+
"""Return Element for chemical symbol (case-insensitive)."""
|
|
180
|
+
key = _normalize_symbol(symbol)
|
|
181
|
+
if key not in _BY_SYMBOL:
|
|
182
|
+
raise KeyError(f"Unknown element symbol: {symbol}")
|
|
183
|
+
return _BY_SYMBOL[key]
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def element(key: int | str) -> Element:
|
|
187
|
+
"""Return Element from atomic number or symbol string."""
|
|
188
|
+
if isinstance(key, int):
|
|
189
|
+
return element_from_z(key)
|
|
190
|
+
if isinstance(key, str):
|
|
191
|
+
s = key.strip()
|
|
192
|
+
if s.isdigit():
|
|
193
|
+
return element_from_z(int(s))
|
|
194
|
+
return element_from_symbol(s)
|
|
195
|
+
raise TypeError("Element key must be int (Z) or str (symbol).")
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
def atomic_number(symbol: str) -> int:
|
|
199
|
+
"""Return atomic number for symbol."""
|
|
200
|
+
return element_from_symbol(symbol).z
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
def atomic_symbol(z: int) -> str:
|
|
204
|
+
"""Return symbol for atomic number."""
|
|
205
|
+
return element_from_z(z).symbol
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def atomic_weight(key: int | str) -> float:
|
|
209
|
+
"""Return atomic mass (atomic weight) for atomic number or symbol."""
|
|
210
|
+
return element(key).atomic_mass
|
|
211
|
+
|
|
212
|
+
|
|
213
|
+
def has_element(key: int | str) -> bool:
|
|
214
|
+
"""Return True if key is a known element symbol or atomic number."""
|
|
215
|
+
try:
|
|
216
|
+
element(key)
|
|
217
|
+
return True
|
|
218
|
+
except (TypeError, ValueError, KeyError):
|
|
219
|
+
return False
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
__all__ = [
|
|
223
|
+
"Element",
|
|
224
|
+
"all_elements",
|
|
225
|
+
"element",
|
|
226
|
+
"element_from_z",
|
|
227
|
+
"element_from_symbol",
|
|
228
|
+
"atomic_number",
|
|
229
|
+
"atomic_symbol",
|
|
230
|
+
"atomic_weight",
|
|
231
|
+
"has_element",
|
|
232
|
+
]
|
|
233
|
+
|
otter/data/helpers.py
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"""
|
|
2
|
+
otter/data/helpers.py
|
|
3
|
+
|
|
4
|
+
Shared helpers for tests and example scripts.
|
|
5
|
+
"""
|
|
6
|
+
from __future__ import annotations
|
|
7
|
+
|
|
8
|
+
import math
|
|
9
|
+
import numpy as np
|
|
10
|
+
|
|
11
|
+
from otter.numerics.constants import CM_TO_BOHR
|
|
12
|
+
from otter.data.elements import atomic_weight as element_atomic_weight
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def ion_density_bohr3(rho_g_cc: float, atomic_weight: float) -> float:
|
|
16
|
+
"""Return ion number density (Bohr^-3) from mass density and atomic weight."""
|
|
17
|
+
if atomic_weight <= 0.0:
|
|
18
|
+
raise ValueError("atomic_weight must be positive.")
|
|
19
|
+
avogadro = 6.02214076e23
|
|
20
|
+
n_i_cm3 = rho_g_cc / atomic_weight * avogadro
|
|
21
|
+
return n_i_cm3 / (CM_TO_BOHR ** 3)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def resolve_atomic_weight(spec: float | int | str) -> float:
|
|
25
|
+
"""
|
|
26
|
+
Resolve atomic weight from a direct mass, atomic number, or symbol.
|
|
27
|
+
|
|
28
|
+
Rules
|
|
29
|
+
-----
|
|
30
|
+
- float -> treated as explicit atomic weight.
|
|
31
|
+
- int -> treated as atomic number Z.
|
|
32
|
+
- str -> symbol (e.g. \"Al\") or integer string (e.g. \"13\").
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(spec, bool):
|
|
35
|
+
raise TypeError("Atomic weight spec must not be bool.")
|
|
36
|
+
if isinstance(spec, float):
|
|
37
|
+
if spec <= 0.0:
|
|
38
|
+
raise ValueError("Atomic weight must be positive.")
|
|
39
|
+
return float(spec)
|
|
40
|
+
if isinstance(spec, int):
|
|
41
|
+
return float(element_atomic_weight(spec))
|
|
42
|
+
if isinstance(spec, str):
|
|
43
|
+
s = spec.strip()
|
|
44
|
+
if not s:
|
|
45
|
+
raise ValueError("Atomic weight spec string cannot be empty.")
|
|
46
|
+
if s.replace(".", "", 1).isdigit() and "." in s:
|
|
47
|
+
val = float(s)
|
|
48
|
+
if val <= 0.0:
|
|
49
|
+
raise ValueError("Atomic weight must be positive.")
|
|
50
|
+
return val
|
|
51
|
+
return float(element_atomic_weight(s))
|
|
52
|
+
raise TypeError("Atomic weight spec must be float, int(Z), or str(symbol/Z).")
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def mu_guess_from_density(n_i: float, zbar: float = 3.0) -> float:
|
|
56
|
+
"""Return a free-electron mu guess from ion density and target Zbar."""
|
|
57
|
+
n0 = zbar * n_i
|
|
58
|
+
return 0.5 * (3.0 * np.pi**2 * n0) ** (2.0 / 3.0)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def grid_step_from_r(r: np.ndarray, grid_kind: str) -> float:
|
|
62
|
+
"""Return the grid step for the specified grid kind."""
|
|
63
|
+
if r.size < 2:
|
|
64
|
+
raise ValueError("Grid must have at least two points.")
|
|
65
|
+
grid_kind = str(grid_kind).lower()
|
|
66
|
+
if grid_kind == "log":
|
|
67
|
+
return float(np.log(r[1] / r[0]))
|
|
68
|
+
if grid_kind == "sqrt":
|
|
69
|
+
return float(np.sqrt(r[1]) - np.sqrt(r[0]))
|
|
70
|
+
if grid_kind == "linear":
|
|
71
|
+
return float(r[1] - r[0])
|
|
72
|
+
raise ValueError(f"Unsupported grid_kind: {grid_kind}")
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def trapz_integral(y: np.ndarray, x: np.ndarray) -> float:
|
|
76
|
+
"""Return a version-independent one-dimensional trapezoidal integral."""
|
|
77
|
+
y_arr = np.asarray(y, dtype=float)
|
|
78
|
+
x_arr = np.asarray(x, dtype=float)
|
|
79
|
+
if y_arr.ndim != 1 or x_arr.ndim != 1 or y_arr.size != x_arr.size:
|
|
80
|
+
raise ValueError("trapz_integral requires equal one-dimensional arrays.")
|
|
81
|
+
if y_arr.size < 2:
|
|
82
|
+
return 0.0
|
|
83
|
+
return float(
|
|
84
|
+
np.sum(0.5 * (y_arr[1:] + y_arr[:-1]) * np.diff(x_arr))
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def ion_level_weight(energy: float, gamma: float) -> float:
|
|
89
|
+
"""Return the M(e) weight from :cite:`StarrettSaumon2013`, Eq. (81)."""
|
|
90
|
+
if gamma <= 0.0:
|
|
91
|
+
return 1.0
|
|
92
|
+
arg = -2.0 * math.sqrt(math.log(2.0)) * energy / gamma
|
|
93
|
+
m_val = math.erf(arg)
|
|
94
|
+
return float(min(max(m_val, 0.0), 1.0))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def electron_count(r: np.ndarray,
|
|
98
|
+
n: np.ndarray,
|
|
99
|
+
r_ws: float | None = None,
|
|
100
|
+
r_min: float | None = None) -> float:
|
|
101
|
+
"""Integrate electron density on r (optionally between r_min and r_ws)."""
|
|
102
|
+
r = np.asarray(r, dtype=float)
|
|
103
|
+
n = np.asarray(n, dtype=float)
|
|
104
|
+
mask = np.ones_like(r, dtype=bool)
|
|
105
|
+
if r_min is not None:
|
|
106
|
+
mask &= r >= float(r_min)
|
|
107
|
+
if r_ws is not None:
|
|
108
|
+
mask &= r <= float(r_ws)
|
|
109
|
+
if not np.any(mask):
|
|
110
|
+
return 0.0
|
|
111
|
+
integrand = n[mask] * r[mask] ** 2
|
|
112
|
+
if hasattr(np, "trapezoid"):
|
|
113
|
+
integral = np.trapezoid(integrand, r[mask])
|
|
114
|
+
else:
|
|
115
|
+
integral = np.trapz(integrand, r[mask])
|
|
116
|
+
return float(4.0 * np.pi * integral)
|
|
117
|
+
|
|
118
|
+
|
|
119
|
+
def tail_stats(r: np.ndarray, n_full: np.ndarray, n_ext: np.ndarray) -> dict:
|
|
120
|
+
"""Return tail means for full/ext/pa/scr densities."""
|
|
121
|
+
tail_mask = r >= 0.7 * r[-1]
|
|
122
|
+
if not np.any(tail_mask):
|
|
123
|
+
tail_mask = slice(None)
|
|
124
|
+
n_pa = n_full - n_ext
|
|
125
|
+
n_scr = n_pa
|
|
126
|
+
return {
|
|
127
|
+
"n_full": float(np.mean(n_full[tail_mask])),
|
|
128
|
+
"n_ext": float(np.mean(n_ext[tail_mask])),
|
|
129
|
+
"n_pa": float(np.mean(n_pa[tail_mask])),
|
|
130
|
+
"n_scr": float(np.mean(n_scr[tail_mask])),
|
|
131
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
"""Electronic-structure solvers and average-atom workflow drivers."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from otter.electronic.full_external import FullExternalConfig, solve_full_only, solve_full_then_external
|
|
5
|
+
from otter.electronic.ks_dft import KSDTFConfig, solve_ks_dft_is
|
|
6
|
+
from otter.electronic.mixture import MixtureConfig, solve_mixture_full_then_ext, solve_mixture_full_only
|
|
7
|
+
from otter.electronic.thomas_fermi import (
|
|
8
|
+
FermiHalfEvaluator,
|
|
9
|
+
ThomasFermiConfig,
|
|
10
|
+
incomplete_fermi_half_lower,
|
|
11
|
+
incomplete_fermi_half_upper,
|
|
12
|
+
solve_thomas_fermi_full_then_external,
|
|
13
|
+
)
|
|
14
|
+
from otter.electronic.xc import xc_provenance
|
|
15
|
+
|
|
16
|
+
__all__ = [
|
|
17
|
+
"FermiHalfEvaluator",
|
|
18
|
+
"FullExternalConfig",
|
|
19
|
+
"KSDTFConfig",
|
|
20
|
+
"MixtureConfig",
|
|
21
|
+
"ThomasFermiConfig",
|
|
22
|
+
"incomplete_fermi_half_lower",
|
|
23
|
+
"incomplete_fermi_half_upper",
|
|
24
|
+
"solve_full_only",
|
|
25
|
+
"solve_full_then_external",
|
|
26
|
+
"solve_ks_dft_is",
|
|
27
|
+
"solve_mixture_full_only",
|
|
28
|
+
"solve_mixture_full_then_ext",
|
|
29
|
+
"solve_thomas_fermi_full_then_external",
|
|
30
|
+
"xc_provenance",
|
|
31
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""Continuum-electron models."""
|
|
2
|
+
from __future__ import annotations
|
|
3
|
+
|
|
4
|
+
from otter.electronic.continuum.hybrid import QuantumContinuumHybrid
|
|
5
|
+
from otter.electronic.continuum.ideal import IdealContinuum, ideal_unbound_density
|
|
6
|
+
from otter.electronic.continuum.scattering import (
|
|
7
|
+
QuantumContinuumFree,
|
|
8
|
+
QuantumContinuumScattering,
|
|
9
|
+
continuum_density_free,
|
|
10
|
+
continuum_density_scattering,
|
|
11
|
+
continuum_density_scattering_adaptive,
|
|
12
|
+
continuum_density_scattering_basis,
|
|
13
|
+
fermi_dirac,
|
|
14
|
+
gamma_from_phase_shift_cache,
|
|
15
|
+
)
|
|
16
|
+
from otter.electronic.continuum.tail import apply_tail_match, tail_parameters
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"IdealContinuum",
|
|
20
|
+
"QuantumContinuumFree",
|
|
21
|
+
"QuantumContinuumHybrid",
|
|
22
|
+
"QuantumContinuumScattering",
|
|
23
|
+
"apply_tail_match",
|
|
24
|
+
"continuum_density_free",
|
|
25
|
+
"continuum_density_scattering",
|
|
26
|
+
"continuum_density_scattering_adaptive",
|
|
27
|
+
"continuum_density_scattering_basis",
|
|
28
|
+
"fermi_dirac",
|
|
29
|
+
"gamma_from_phase_shift_cache",
|
|
30
|
+
"ideal_unbound_density",
|
|
31
|
+
"tail_parameters",
|
|
32
|
+
]
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""
|
|
2
|
+
otter/electronic/continuum/hybrid.py
|
|
3
|
+
|
|
4
|
+
Purpose
|
|
5
|
+
-------
|
|
6
|
+
Provide a hybrid continuum model: quantum scattering at low energies
|
|
7
|
+
plus an ideal-gas high-energy tail.
|
|
8
|
+
|
|
9
|
+
Methods
|
|
10
|
+
-------
|
|
11
|
+
- Compute low-energy continuum with Numerov scattering (A3).
|
|
12
|
+
- Add a uniform high-energy tail using the ideal free-electron DOS.
|
|
13
|
+
- Default n0_mode uses an integral-based density for consistency.
|
|
14
|
+
|
|
15
|
+
Equations
|
|
16
|
+
---------
|
|
17
|
+
Ideal DOS contribution (atomic units):
|
|
18
|
+
n = (sqrt(2)/pi^2) * integral_{e0}^{e1} e^(1/2) f(e; mu, T) de
|
|
19
|
+
|
|
20
|
+
Hybrid density:
|
|
21
|
+
n_c(r) = n_low(r; e in [e_min, e_cut]) + n_high (uniform)
|
|
22
|
+
|
|
23
|
+
References
|
|
24
|
+
----------
|
|
25
|
+
- :cite:`StarrettSaumon2014`, Appendix A/B. The high-energy ideal-gas tail
|
|
26
|
+
and its splice controls are Otter numerical implementation choices.
|
|
27
|
+
"""
|
|
28
|
+
from __future__ import annotations
|
|
29
|
+
|
|
30
|
+
from typing import Dict
|
|
31
|
+
import numpy as np
|
|
32
|
+
|
|
33
|
+
from .interface import ContinuumModel
|
|
34
|
+
from .scattering import fermi_dirac, QuantumContinuumScattering
|
|
35
|
+
from .ideal import ideal_unbound_density
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def _trapz(y: np.ndarray, x: np.ndarray) -> np.ndarray:
|
|
39
|
+
if hasattr(np, "trapezoid"):
|
|
40
|
+
return np.trapezoid(y, x)
|
|
41
|
+
return np.trapz(y, x)
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def ideal_density_range(mu: float,
|
|
45
|
+
temperature: float,
|
|
46
|
+
e_min: float,
|
|
47
|
+
e_max: float,
|
|
48
|
+
n_samples: int = 800) -> float:
|
|
49
|
+
"""
|
|
50
|
+
Integrate the ideal free-electron density over a finite energy range.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
mu : float
|
|
55
|
+
Chemical potential (Ha).
|
|
56
|
+
temperature : float
|
|
57
|
+
Temperature (Ha).
|
|
58
|
+
e_min : float
|
|
59
|
+
Lower energy bound (Ha).
|
|
60
|
+
e_max : float
|
|
61
|
+
Upper energy bound (Ha).
|
|
62
|
+
n_samples : int
|
|
63
|
+
Number of energy samples.
|
|
64
|
+
|
|
65
|
+
Returns
|
|
66
|
+
-------
|
|
67
|
+
float
|
|
68
|
+
Density contribution over [e_min, e_max] (Bohr^-3).
|
|
69
|
+
"""
|
|
70
|
+
if e_max <= e_min:
|
|
71
|
+
return 0.0
|
|
72
|
+
e_min = max(float(e_min), 0.0)
|
|
73
|
+
e_max = float(e_max)
|
|
74
|
+
n_samples = max(int(n_samples), 16)
|
|
75
|
+
|
|
76
|
+
e_grid = np.linspace(e_min, e_max, n_samples)
|
|
77
|
+
occ = fermi_dirac(e_grid, mu, temperature)
|
|
78
|
+
integrand = np.sqrt(np.maximum(e_grid, 0.0)) * occ
|
|
79
|
+
pref = np.sqrt(2.0) / (np.pi ** 2)
|
|
80
|
+
return float(pref * _trapz(integrand, e_grid))
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class QuantumContinuumHybrid(ContinuumModel):
|
|
84
|
+
"""
|
|
85
|
+
Hybrid continuum: scattering at low energy + ideal high-energy tail.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
def density(self,
|
|
89
|
+
r: np.ndarray,
|
|
90
|
+
mu: float,
|
|
91
|
+
temperature: float,
|
|
92
|
+
params: Dict[str, float] | None = None) -> np.ndarray:
|
|
93
|
+
params = params or {}
|
|
94
|
+
|
|
95
|
+
v_eff = params.get("v_eff", None)
|
|
96
|
+
if v_eff is None:
|
|
97
|
+
raise ValueError("params['v_eff'] is required for hybrid continuum.")
|
|
98
|
+
|
|
99
|
+
e_min = float(params.get("e_min", 1e-6))
|
|
100
|
+
e_cut = float(params.get("e_cut", max(mu + 4.0 * temperature, 1.0)))
|
|
101
|
+
e_high_max = float(params.get("e_high_max", max(e_cut + 20.0 * temperature, mu + 30.0 * temperature)))
|
|
102
|
+
n_e_low = int(params.get("n_e_low", 120))
|
|
103
|
+
n_e_high = int(params.get("n_e_high", 400))
|
|
104
|
+
|
|
105
|
+
n0_mode = str(params.get("n0_mode", "integral")).lower()
|
|
106
|
+
if n0_mode not in ("ideal", "integral"):
|
|
107
|
+
raise ValueError("n0_mode must be 'ideal' or 'integral'.")
|
|
108
|
+
|
|
109
|
+
scatter = QuantumContinuumScattering()
|
|
110
|
+
low_params = dict(params)
|
|
111
|
+
low_params["e_max"] = e_cut
|
|
112
|
+
low_params["n_e"] = n_e_low
|
|
113
|
+
n_low = scatter.density(r, mu, temperature, params=low_params)
|
|
114
|
+
|
|
115
|
+
if n0_mode == "ideal":
|
|
116
|
+
n0_total = ideal_unbound_density(mu, temperature)
|
|
117
|
+
n0_low = ideal_density_range(mu, temperature, e_min, e_cut, n_samples=n_e_high)
|
|
118
|
+
n_high = max(n0_total - n0_low, 0.0)
|
|
119
|
+
else:
|
|
120
|
+
n0_total = ideal_density_range(mu, temperature, 0.0, e_high_max, n_samples=n_e_high)
|
|
121
|
+
n_high = ideal_density_range(mu, temperature, e_cut, e_high_max, n_samples=n_e_high)
|
|
122
|
+
|
|
123
|
+
n_hybrid = n_low + n_high
|
|
124
|
+
return n_hybrid
|