Skip to content

Commit b0064cb

Browse files
committed
Merge branch 'master' into python-3.14
2 parents c4b01ac + 9c2fdf7 commit b0064cb

File tree

9 files changed

+70
-37
lines changed

9 files changed

+70
-37
lines changed

.github/workflows/test.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -118,8 +118,8 @@ jobs:
118118
PYTHONWARNDEFAULTENCODING: "true" # PEP 597: Enable optional EncodingWarning
119119
run: |
120120
pytest tests \
121-
--splits 10 --group ${{ matrix.split }} \
122-
--durations-path tests/files/.pytest-split-durations
121+
-n auto --splits 10 --group ${{ matrix.split }} \
122+
--durations-path tests/files/.pytest-split-durations \
123123
124124
trigger_atomate2_ci:
125125
needs: test

pyproject.toml

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ include = ["pymatgen", "pymatgen.*"]
167167

168168
[tool.pdm.dev-dependencies]
169169
lint = ["mypy>=1.10.0", "pre-commit>=3.7.1", "ruff>=0.4.9"]
170-
test = ["pytest-cov>=5.0.0", "pytest-split>=0.9.0", "pytest>=8.2.2"]
170+
test = ["pytest-cov>=5.0.0", "pytest-split>=0.9.0", "pytest>=8.2.2","pytest-xdist>=3.0.0"]
171171

172172
[tool.cibuildwheel.linux]
173173
archs = ["auto64"]
@@ -259,7 +259,7 @@ docstring-code-format = true
259259
"dev_scripts/*" = ["D"]
260260

261261
[tool.pytest.ini_options]
262-
addopts = "--durations=30 --quiet -r xXs --color=yes --import-mode=importlib"
262+
addopts = "-n auto --durations=30 --quiet -r xXs --color=yes --import-mode=importlib"
263263
filterwarnings = [
264264
# NOTE: the LAST matching option would be used
265265
"ignore::UserWarning",
@@ -334,7 +334,8 @@ dev = [
334334
test = [
335335
"pytest>=8.3.5",
336336
"pytest-cov>=6.0.0",
337-
"pytest-split>=0.10.0"
337+
"pytest-split>=0.10.0",
338+
"pytest-xdist>=3.0.0"
338339
]
339340
lint = [
340341
"mypy>=1.15.0",

src/pymatgen/analysis/structure_prediction/dopant_predictor.py

Lines changed: 16 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -127,26 +127,21 @@ def get_dopants_from_shannon_radii(bonded_structure, num_dopants=5, match_oxi_si
127127

128128
def _get_dopants(substitutions, num_dopants, match_oxi_sign) -> dict:
129129
"""Utility method to get n- and p-type dopants from a list of substitutions."""
130-
n_type = [
131-
pred
132-
for pred in substitutions
133-
if pred["dopant_species"].oxi_state > pred["original_species"].oxi_state
134-
and (
135-
not match_oxi_sign
136-
or np.sign(pred["dopant_species"].oxi_state) == np.sign(pred["original_species"].oxi_state)
137-
)
138-
]
139-
p_type = [
140-
pred
141-
for pred in substitutions
142-
if pred["dopant_species"].oxi_state < pred["original_species"].oxi_state
143-
and (
144-
not match_oxi_sign
145-
or np.sign(pred["dopant_species"].oxi_state) == np.sign(pred["original_species"].oxi_state)
146-
)
147-
]
148-
149-
return {"n_type": n_type[:num_dopants], "p_type": p_type[:num_dopants]}
130+
dopants = {k: [] for k in ("n_type", "p_type")}
131+
for k in dopants: # noqa: PLC0206
132+
for pred in substitutions:
133+
if (
134+
pred["dopant_species"].oxi_state > pred["original_species"].oxi_state
135+
if k == "n_type"
136+
else pred["dopant_species"].oxi_state < pred["original_species"].oxi_state
137+
) and (
138+
not match_oxi_sign
139+
or np.sign(pred["dopant_species"].oxi_state) == np.sign(pred["original_species"].oxi_state)
140+
):
141+
dopants[k].append(pred)
142+
if len(dopants[k]) == num_dopants:
143+
break
144+
return dopants
150145

151146

152147
def _shannon_radii_from_cn(species_list, cn_roman, radius_to_compare=0):
@@ -171,7 +166,7 @@ def _shannon_radii_from_cn(species_list, cn_roman, radius_to_compare=0):
171166
172167
- "species": The species with charge state.
173168
- "radius": The Shannon radius for the species.
174-
- "radius_diff": The difference between the Shannon radius and the
169+
- "radii_diff": The difference between the Shannon radius and the
175170
radius_to_compare optional argument.
176171
"""
177172
shannon_radii = []

src/pymatgen/cli/pmg.py

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -62,15 +62,21 @@ def format_lists(v):
6262
["---------------", "", ""],
6363
]
6464
output += [
65-
[k, format_lists(diff["Same"][k]), format_lists(diff["Same"][k])] for k in sorted(diff["Same"]) if k != "SYSTEM"
65+
( # type: ignore[misc]
66+
k,
67+
format_lists(diff["Same"][k]),
68+
format_lists(diff["Same"][k]),
69+
)
70+
for k in sorted(diff["Same"])
71+
if k != "SYSTEM"
6672
]
6773
output += [
6874
["", "", ""],
6975
["DIFFERENT PARAMS", "", ""],
7076
["----------------", "", ""],
7177
]
7278
output += [
73-
[
79+
[ # type: ignore[misc]
7480
k,
7581
format_lists(diff["Different"][k]["INCAR1"]),
7682
format_lists(diff["Different"][k]["INCAR2"]),

src/pymatgen/core/composition.py

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -370,7 +370,7 @@ def formula(self) -> str:
370370
e.g. Li4 Fe4 P4 O16.
371371
"""
372372
sym_amt = self.get_el_amt_dict()
373-
syms = sorted(sym_amt, key=lambda sym: get_el_sp(sym).X)
373+
syms = _symbol_sort_order(sym_amt)
374374
formula = [f"{s}{formula_double_format(sym_amt[s], ignore_ones=False)}" for s in syms]
375375
return " ".join(formula)
376376

@@ -550,7 +550,7 @@ def num_atoms(self) -> float:
550550
@property
551551
def weight(self) -> FloatWithUnit:
552552
"""Total molecular weight of Composition."""
553-
return Mass(sum([amount * el.atomic_mass for el, amount in self.items()], 0.0), _PT_UNIT["Atomic mass"])
553+
return Mass(sum(amount * el.atomic_mass for el, amount in self.items()), _PT_UNIT["Atomic mass"]) # type: ignore[misc]
554554

555555
def get_atomic_fraction(self, el: SpeciesLike) -> float:
556556
"""Calculate atomic fraction of an Element or Species.
@@ -1363,7 +1363,7 @@ def reduce_formula(
13631363
Returns:
13641364
tuple[str, int]: reduced formula and factor.
13651365
"""
1366-
syms: list[str] = sorted(sym_amt, key=lambda x: [get_el_sp(x).X, x])
1366+
syms: list[str] = _symbol_sort_order(sym_amt)
13671367

13681368
syms = list(filter(lambda x: abs(sym_amt[x]) > Composition.amount_tolerance, syms))
13691369

@@ -1394,6 +1394,11 @@ def reduce_formula(
13941394
return "".join([*reduced_form, *poly_anions]), factor
13951395

13961396

1397+
def _symbol_sort_order(sym: list[str] | Mapping[str, Any]) -> list[str]:
1398+
"""Define fixed order for sorting based on electronegativity + alphabatical fallback."""
1399+
return sorted(sym, key=lambda x: [float("inf") if math.isnan(e_neg := get_el_sp(x).X) else e_neg, x])
1400+
1401+
13971402
class CompositionError(Exception):
13981403
"""Composition exceptions."""
13991404

src/pymatgen/io/vasp/inputs.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1220,7 +1220,7 @@ def __init__(
12201220
style: KpointsSupportedModes = supported_modes.Gamma,
12211221
kpts: Sequence[Kpoint] = ((1, 1, 1),),
12221222
kpts_shift: tuple[float, float, float] = (0, 0, 0),
1223-
kpts_weights: list[float] | None = None,
1223+
kpts_weights: Sequence[float] | None = None,
12241224
coord_type: Literal["Reciprocal", "Cartesian"] | None = None,
12251225
labels: list[str] | None = None,
12261226
tet_number: int = 0,
@@ -1246,12 +1246,12 @@ def __init__(
12461246
(or negative), VASP automatically generates the KPOINTS.
12471247
style: Style for generating KPOINTS. Use one of the
12481248
Kpoints.supported_modes enum types.
1249-
kpts (2D array): Array of kpoints. Even when only a single
1249+
kpts (2D sequence): Sequence of kpoints. Even when only a single
12501250
specification is required, e.g. in the automatic scheme,
1251-
the kpts should still be specified as a 2D array. e.g.
1251+
the kpts should still be specified as a 2D sequence. e.g.
12521252
[(20,),] or [(2, 2, 2),].
12531253
kpts_shift (3x1 array): Shift for kpoints.
1254-
kpts_weights (list[float]): Optional weights for explicit kpoints.
1254+
kpts_weights (Sequence[float]): Optional weights for explicit kpoints.
12551255
coord_type: In line-mode, this variable specifies whether the
12561256
Kpoints were given in Cartesian or Reciprocal coordinates.
12571257
labels: In line-mode, this should provide a list of labels for
@@ -1266,15 +1266,15 @@ def __init__(
12661266
Format is a list of tuples, [ (sym_weight, [tet_vertices]),
12671267
...]
12681268
"""
1269-
if num_kpts > 0 and not labels and not kpts_weights:
1269+
if num_kpts > 0 and not labels and kpts_weights is None:
12701270
raise ValueError("For explicit or line-mode kpoints, either the labels or kpts_weights must be specified.")
12711271

12721272
self.comment = comment
12731273
self.num_kpts = num_kpts
12741274
self.kpts = kpts # type: ignore[assignment]
12751275
self.style = style
12761276
self.coord_type = coord_type
1277-
self.kpts_weights = kpts_weights
1277+
self.kpts_weights = list(kpts_weights) if kpts_weights is not None else None
12781278
self.kpts_shift = tuple(kpts_shift)
12791279
self.labels = labels
12801280
self.tet_number = tet_number

tests/core/test_composition.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -869,6 +869,26 @@ def test_curly_bracket_deeply_nested_formulas(self):
869869
}.items():
870870
assert Composition(formula).formula == expected
871871

872+
def test_formula_order(self):
873+
ref_vals = {
874+
"formula": "Zn2 C2 Br2 Ar2 Ne2",
875+
"reduced_formula": "ZnCBrArNe",
876+
}
877+
assert all(
878+
all(getattr(Composition(dict.fromkeys(ele_order, 2)), k) == v for k, v in ref_vals.items())
879+
for ele_order in [
880+
("Br", "Ar", "Zn", "C", "Ne"),
881+
(
882+
"Ne",
883+
"Br",
884+
"Ar",
885+
"Zn",
886+
"C",
887+
),
888+
("Ar", "Br", "Ne", "C", "Zn"),
889+
]
890+
)
891+
872892

873893
def test_reduce_formula():
874894
assert reduce_formula({"Li": 2, "Mn": 4, "O": 8}) == ("LiMn2O4", 2)

tests/io/vasp/test_inputs.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1112,6 +1112,11 @@ def test_init(self):
11121112
0.5 0.5 0.5 4 None"""
11131113
assert str(kpoints).strip() == expected_kpt_str
11141114

1115+
# ensure KPOINTS deserialize even when weights are numpy array
1116+
conf_dict = kpoints.as_dict()
1117+
conf_dict["kpts_weights"] = np.array(conf_dict["kpts_weights"])
1118+
assert Kpoints.from_dict(conf_dict) == kpoints
1119+
11151120
# Explicit tetrahedra method
11161121
filepath = f"{VASP_IN_DIR}/KPOINTS_explicit_tet"
11171122
kpoints = Kpoints.from_file(filepath)

tests/performance/test_import_time.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ def test_get_ref_import_time() -> None:
7979
pytest.fail("Reference import times generated. Copy from output to update JSON file.")
8080

8181

82+
@pytest.mark.xfail(reason="High variance in CI run times.")
8283
@pytest.mark.skipif(GEN_REF_TIME, reason="Generating reference import time.")
8384
@pytest.mark.parametrize(
8485
("grace_percent", "hard_percent"),

0 commit comments

Comments
 (0)