Skip to content

Commit 9c4c1ef

Browse files
authored
[MNT] Refactor and speed up Sanderson's electronegativity calculation (#233)
* refator sanderson's electronegativity * 💄
1 parent a19f676 commit 9c4c1ef

File tree

3 files changed

+118
-65
lines changed

3 files changed

+118
-65
lines changed

mendeleev/electronegativity.py

+32
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,38 @@ def n_effective(n: int, source: str = "slater") -> Union[float, None]:
4343
)
4444

4545

46+
def interpolate_property(
47+
x: int, x_ref: List[int], y_ref: List[float], poly_deg: int = 1
48+
) -> float:
49+
"""
50+
Estiate a property for element by interpolation or
51+
extrapolation of the data points from x`x_ref` and `y_ref`.
52+
53+
Args:
54+
x: value for which the property will be evaluated
55+
x_ref: list of values for the elements
56+
y_ref: list of values of the property for the elements
57+
deg: degree of the polynomial used in the extrapolation beyond
58+
the provided data points, default=1
59+
"""
60+
x_ref = np.array(x_ref)
61+
y_ref = np.array(y_ref)
62+
if x_ref.min() <= x <= x_ref.max():
63+
return np.interp([x], x_ref, y_ref)
64+
65+
# extrapolation
66+
if x < x_ref.min():
67+
x_slice = x_ref[:3]
68+
y_slice = y_ref[:3]
69+
elif x > x_ref.max():
70+
x_slice = x_ref[-3:]
71+
y_slice = y_ref[-3:]
72+
73+
fit = np.polyfit(x_slice, y_slice, poly_deg)
74+
fn = np.poly1d(fit)
75+
return fn(x)
76+
77+
4678
def allred_rochow(zeff: float, radius: float) -> float:
4779
"""
4880
Calculate the electronegativity of an atom according to the definition

mendeleev/models.py

+43-63
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
martynov_batsanov,
2626
mulliken,
2727
sanderson,
28+
interpolate_property,
2829
)
2930
from .db import get_session
3031
from .econf import ElectronicConfiguration, get_l, ORBITALS
@@ -774,15 +775,21 @@ def electronegativity_pauling(self) -> float:
774775
"Pauling's electronegativity"
775776
return self.en_pauling
776777

777-
def electronegativity_sanderson(self, radius="covalent_radius_pyykko") -> float:
778+
def electronegativity_sanderson(
779+
self, radius: str = "covalent_radius_pyykko"
780+
) -> float:
778781
"""
779-
Sanderson electronegativity
782+
Sanderson's electronegativity
780783
781784
Args:
782785
radius : radius to use in the calculation
783786
"""
784-
# estimate the radius of a corresponding noble gas
785-
noble_gas_radius = estimate_from_group(self.atomic_number, radius)
787+
results = fetch_by_group(["atomic_number", radius], group=18)
788+
# transpose rows to list of properties
789+
atomic_numbers, radii = list(zip(*results))
790+
noble_gas_radius = interpolate_property(
791+
self.atomic_number, atomic_numbers, radii
792+
)
786793
return sanderson(getattr(self, radius), noble_gas_radius)
787794

788795
def nvalence(self, method: str = None) -> int:
@@ -841,6 +848,36 @@ def __repr__(self) -> str:
841848
)
842849

843850

851+
def fetch_by_group(properties: List[str], group: int = 18) -> tuple[list[Any]]:
852+
"""
853+
Get a specified properties for all the elements of a given group.
854+
855+
Args:
856+
properties: Attributes of `Element` to retrieve for all group members
857+
group: Group number to retrieve data for
858+
859+
Returns:
860+
result: list of tuples with the requested properties for all group members
861+
"""
862+
if isinstance(properties, str):
863+
props = [properties]
864+
else:
865+
props = properties[:]
866+
867+
if "atomic_number" not in props:
868+
props = ["atomic_number"] + props
869+
870+
session = get_session()
871+
results = (
872+
session.query(*[getattr(Element, prop) for prop in props])
873+
.filter(Element.group_id == group)
874+
.order_by("atomic_number")
875+
.all()
876+
)
877+
session.close()
878+
return results
879+
880+
844881
class ValueOrigin(enum.Enum):
845882
"Options for the origin of the property value."
846883

@@ -889,65 +926,6 @@ def __repr__(self) -> str:
889926
)
890927

891928

892-
def fetch_attrs_for_group(attrs: List[str], group: int = 18) -> Tuple[List[Any]]:
893-
"""
894-
A convenience function for getting a specified attribute for all
895-
the memebers of a given group.
896-
897-
Args:
898-
attr : Attribute of `Element` to retrieve for all group members
899-
900-
Returns:
901-
data (dict): Dictionary with noble gas atomic numbers as keys and values of the
902-
`attr` as values
903-
"""
904-
session = get_session()
905-
members = (
906-
session.query(Element)
907-
.filter(Element.group_id == group)
908-
.order_by(Element.atomic_number)
909-
.all()
910-
)
911-
912-
results = tuple([getattr(member, attr) for member in members] for attr in attrs)
913-
session.close()
914-
return results
915-
916-
917-
def estimate_from_group(
918-
atomic_number, attr_name, group: int = 18, deg: int = 1
919-
) -> float:
920-
"""
921-
Evaluate a value `attribute` for element by interpolation or
922-
extrapolation of the data points from elements from `group`.
923-
924-
Args:
925-
atomic_number: value for which the property will be evaluated
926-
attr_name: attribute to be estimated
927-
group: periodic table group number
928-
deg: degree of the polynomial used in the extrapolation beyond
929-
the provided data points
930-
"""
931-
xref, yref = fetch_attrs_for_group(["atomic_number", attr_name], group=group)
932-
933-
x = atomic_number
934-
xref = np.array(xref)
935-
yref = np.array(yref)
936-
if xref.min() <= x <= xref.max():
937-
return np.interp([x], xref, yref)
938-
939-
if x < xref.min():
940-
xslice = xref[:3]
941-
yslice = yref[:3]
942-
elif x > xref.max():
943-
xslice = xref[-3:]
944-
yslice = yref[-3:]
945-
946-
fit = np.polyfit(xslice, yslice, deg)
947-
fn = np.poly1d(fit)
948-
return fn(x)
949-
950-
951929
class IonicRadius(Base):
952930
"""
953931
Effective ionic radii and crystal radii in pm retrieved from [1]_.
@@ -1130,6 +1108,7 @@ def __repr__(self) -> str:
11301108
return "<Series(name={n:s}, color={c:s})>".format(n=self.name, c=self.color)
11311109

11321110

1111+
# TODO: move to utils
11331112
def with_uncertainty(value: float, uncertainty: float, digits: int = 5) -> str:
11341113
"""Format a value with uncertainty using scientific notation.
11351114
@@ -1346,6 +1325,7 @@ def __repr__(self) -> str:
13461325
return str(self)
13471326

13481327

1328+
# TODO: some thing is wrong with the docstring
13491329
class ScatteringFactor(Base):
13501330
"""Atomic scattering factors
13511331

tests/test_electronegativity.py

+43-2
Original file line numberDiff line numberDiff line change
@@ -2,16 +2,17 @@
22

33
from mendeleev.models import Element
44
from mendeleev.electronegativity import (
5-
n_effective,
65
allred_rochow,
76
cottrell_sutton,
7+
generic,
88
gordy,
9+
interpolate_property,
910
li_xue,
1011
martynov_batsanov,
1112
mulliken,
13+
n_effective,
1214
nagle,
1315
sanderson,
14-
generic,
1516
)
1617

1718

@@ -74,3 +75,43 @@ def test_sanderson():
7475
def test_generic():
7576
assert generic(4.0, 2.0, 2, 1) == pytest.approx(1.0)
7677
assert generic(9.0, 3.0, 2, 0.5) == pytest.approx(1.0)
78+
79+
80+
def test_interpolation_within_range():
81+
x_ref = [1, 2, 4, 5]
82+
y_ref = [10, 20, 40, 50]
83+
x = 3
84+
result = interpolate_property(x, x_ref, y_ref)
85+
assert pytest.approx(result, 0.0001) == 30.0
86+
87+
88+
def test_extrapolation_below_range():
89+
x_ref = [2, 3, 4, 5]
90+
y_ref = [20, 30, 40, 50]
91+
x = 1
92+
result = interpolate_property(x, x_ref, y_ref)
93+
assert pytest.approx(result, 1e-1) == 10.0
94+
95+
96+
def test_extrapolation_above_range():
97+
x_ref = [1, 2, 3, 4]
98+
y_ref = [10, 20, 30, 40]
99+
x = 5
100+
result = interpolate_property(x, x_ref, y_ref)
101+
assert pytest.approx(result, 1e-1) == 50.0
102+
103+
104+
def test_linear_interpolation():
105+
x_ref = [1, 3, 5]
106+
y_ref = [10, 30, 50]
107+
x = 4
108+
result = interpolate_property(x, x_ref, y_ref)
109+
assert pytest.approx(result, 0.0001) == 40.0
110+
111+
112+
def test_invalid_inputs():
113+
x_ref = [1, 2, 3]
114+
y_ref = [10, 20] # Mismatched lengths
115+
x = 2
116+
with pytest.raises(ValueError):
117+
interpolate_property(x, x_ref, y_ref)

0 commit comments

Comments
 (0)