Skip to content

Commit 98b4048

Browse files
authored
Fix sfd nan at large negative x from log(0) (#347)
sfd(x) computes the Fermi-Dirac entropy -f*log(f) - (1-f)*log(1-f). For x <= -39, expm1(x) rounds to -1.0 in float64, so f -> 1.0 and 1-f -> 0.0, producing log(0) -> nan and RuntimeWarnings (divide by zero in log, invalid value in multiply). The physical entropy limit there is 0 (x*log(x) -> 0 as x -> 0). Mask the f=0/1 boundaries to 0 instead of computing the log. Interior values are unchanged. This was a real numerical bug, not just noise: test_band and other tests consumed nan-valued entropy from sfd. Add a regression test covering x in [-100, 100] asserting finiteness and the vanishing boundary. Drop the NaN-tolerating filter in test_sfd_properties since sfd no longer returns nan.
1 parent dbd7391 commit 98b4048

2 files changed

Lines changed: 35 additions & 5 deletions

File tree

dptb/tests/test_occupy.py

Lines changed: 23 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -161,10 +161,29 @@ def test_sfd_properties(self):
161161
x = np.linspace(-5, 5, 100)
162162
s = sfd(x)
163163

164-
# Entropy should be non-negative (handling log singularities)
165-
# Filter out NaN values which can occur at boundaries
166-
s_valid = s[np.isfinite(s)]
167-
assert np.all(s_valid >= 0)
164+
# Entropy should be non-negative
165+
assert np.all(s >= 0)
166+
167+
# Maximum at Fermi level (x=0)
168+
assert np.isclose(s[50], np.max(s), atol=1e-6)
169+
170+
def test_sfd_finite_at_large_negative_x(self):
171+
"""sfd must stay finite for large negative x where expm1 rounds to -1.
172+
173+
For x <= -39, expm1(x) rounds to -1.0 in float64, so f -> 1 and
174+
1-f -> 0. The entropy has physical limit 0 there (x*log(x) -> 0),
175+
so the result must be finite 0 rather than log(0) -> nan.
176+
"""
177+
x = np.array([-100, -50, -40, -39, -20, 0, 20, 40, 50, 100])
178+
s = sfd(x)
179+
assert np.all(np.isfinite(s))
180+
# Extreme values and negative round-to-one boundaries vanish exactly.
181+
assert np.all(s[[0, 1, 2, 3, 8, 9]] == 0.0)
182+
# x=40 is inside the compute range; f is tiny but nonzero, so entropy
183+
# is a small positive number rather than 0
184+
assert s[7] > 0 and s[7] < 1e-10
185+
# interior unchanged: x=0 is the entropy maximum ln(2)
186+
assert np.isclose(s[5], np.log(2.0), rtol=1e-12)
168187

169188
def test_smp1_zero(self):
170189
"""Test that MP1 entropy is zero (by design)."""

dptb/utils/occupy.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,18 @@ def sfd(x):
147147
x_valid = x[mask_in_limit]
148148
f_valid = 1.0 / (np.expm1(x_valid) + 2.0) # stable ffd
149149
f1_valid = 1.0 - f_valid
150-
out[mask_in_limit] = -f_valid * np.log(f_valid) - f1_valid * np.log(f1_valid)
150+
# Guard the f=0/1 boundaries: for sufficiently negative x, expm1(x) rounds
151+
# to -1.0 in float64, so f_valid -> 1.0 and f1_valid -> 0.0, which would
152+
# produce log(0) -> nan. The physical entropy limit there is 0
153+
# (x*log(x) -> 0 as x -> 0), so mask those entries to 0 instead of
154+
# computing the log. The interior values are unchanged.
155+
interior = (f_valid > 0.0) & (f_valid < 1.0)
156+
out_valid = np.zeros_like(f_valid)
157+
out_valid[interior] = (
158+
-f_valid[interior] * np.log(f_valid[interior])
159+
- f1_valid[interior] * np.log(f1_valid[interior])
160+
)
161+
out[mask_in_limit] = out_valid
151162
return out
152163

153164
def fmp1(x):

0 commit comments

Comments
 (0)