Skip to content

Commit 9c0d003

Browse files
authored
Merge branch 'master' into test-monty-reverse-read
2 parents a7bafa2 + 31f1e1f commit 9c0d003

File tree

16 files changed

+681
-448
lines changed

16 files changed

+681
-448
lines changed

src/pymatgen/analysis/magnetism/analyzer.py

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -170,8 +170,7 @@ def __init__(
170170
has_spin = False
171171
for comp in structure.species_and_occu:
172172
for sp in comp:
173-
if getattr(sp, "spin", False):
174-
has_spin = True
173+
has_spin |= bool(getattr(sp, "spin", False))
175174

176175
# perform input sanitation ...
177176
# rest of class will assume magnetic moments are stored on site properties:

src/pymatgen/analysis/xas/spectrum.py

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,12 +10,16 @@
1010
from scipy.interpolate import interp1d
1111

1212
from pymatgen.analysis.structure_matcher import StructureMatcher
13+
from pymatgen.core import Element
1314
from pymatgen.core.spectrum import Spectrum
1415
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer
1516

1617
if TYPE_CHECKING:
18+
from collections.abc import Sequence
1719
from typing import Literal
1820

21+
from pymatgen.core import Structure
22+
1923
__author__ = "Chen Zheng, Yiming Chen"
2024
__copyright__ = "Copyright 2012, The Materials Project"
2125
__version__ = "3.0"
@@ -42,29 +46,31 @@ class XAS(Spectrum):
4246
Attributes:
4347
x (Sequence[float]): The sequence of energies.
4448
y (Sequence[float]): The sequence of mu(E).
45-
absorbing_element (str): The absorbing element of the spectrum.
49+
absorbing_element (str or .Element): The absorbing element of the spectrum.
4650
edge (str): The edge of the spectrum.
4751
spectrum_type (str): The type of the spectrum (XANES or EXAFS).
4852
absorbing_index (int): The absorbing index of the spectrum.
53+
zero_negative_intensity (bool) : Whether to set unphysical negative intensities to zero
4954
"""
5055

5156
XLABEL = "Energy"
5257
YLABEL = "Intensity"
5358

5459
def __init__(
5560
self,
56-
x,
57-
y,
58-
structure,
59-
absorbing_element,
60-
edge="K",
61-
spectrum_type="XANES",
62-
absorbing_index=None,
61+
x: Sequence,
62+
y: Sequence,
63+
structure: Structure,
64+
absorbing_element: str | Element,
65+
edge: str = "K",
66+
spectrum_type: str = "XANES",
67+
absorbing_index: int | None = None,
68+
zero_negative_intensity: bool = False,
6369
):
6470
"""Initialize a spectrum object."""
6571
super().__init__(x, y, structure, absorbing_element, edge)
6672
self.structure = structure
67-
self.absorbing_element = absorbing_element
73+
self.absorbing_element = Element(absorbing_element)
6874
self.edge = edge
6975
self.spectrum_type = spectrum_type
7076
self.e0 = self.x[np.argmax(np.gradient(self.y) / np.gradient(self.x))]
@@ -75,8 +81,16 @@ def __init__(
7581
]
7682
self.absorbing_index = absorbing_index
7783
# check for empty spectra and negative intensities
78-
if sum(1 for i in self.y if i <= 0) / len(self.y) > 0.05:
79-
raise ValueError("Double check the intensities. Most of them are non-positive.")
84+
neg_intens_mask = self.y < 0.0
85+
if len(self.y[neg_intens_mask]) / len(self.y) > 0.05:
86+
warnings.warn(
87+
"Double check the intensities. More than 5% of them are negative.",
88+
UserWarning,
89+
stacklevel=2,
90+
)
91+
self.zero_negative_intensity = zero_negative_intensity
92+
if self.zero_negative_intensity:
93+
self.y[neg_intens_mask] = 0.0
8094

8195
def __str__(self):
8296
return (

src/pymatgen/core/composition.py

Lines changed: 121 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -36,41 +36,80 @@
3636

3737
@total_ordering
3838
class Composition(collections.abc.Hashable, collections.abc.Mapping, MSONable, Stringify):
39-
"""Represents a Composition, which is essentially a {element:amount} mapping
40-
type. Composition is written to be immutable and hashable,
41-
unlike a standard Python dict.
42-
43-
Note that the key can be either an Element or a Species. Elements and Species
44-
are treated differently. i.e., a Fe2+ is not the same as a Fe3+ Species and
45-
would be put in separate keys. This differentiation is deliberate to
46-
support using Composition to determine the fraction of a particular Species.
47-
48-
Works almost completely like a standard python dictionary, except that
49-
__getitem__ is overridden to return 0 when an element is not found.
50-
(somewhat like a defaultdict, except it is immutable).
51-
52-
Also adds more convenience methods relevant to compositions, e.g.
53-
get_fraction.
54-
55-
It should also be noted that many Composition related functionality takes
56-
in a standard string as a convenient input. For example,
57-
even though the internal representation of a Fe2O3 composition is
58-
{Element("Fe"): 2, Element("O"): 3}, you can obtain the amount of Fe
59-
simply by comp["Fe"] instead of the more verbose comp[Element("Fe")].
60-
61-
>>> comp = Composition("LiFePO4")
62-
>>> comp.get_atomic_fraction(Element("Li"))
63-
0.14285714285714285
64-
>>> comp.num_atoms
65-
7.0
66-
>>> comp.reduced_formula
67-
'LiFePO4'
68-
>>> comp.formula
69-
'Li1 Fe1 P1 O4'
70-
>>> comp.get_wt_fraction(Element("Li"))
71-
0.04399794666951898
72-
>>> comp.num_atoms
73-
7.0
39+
"""
40+
Represents a `Composition`, a mapping of {element/species: amount} with
41+
enhanced functionality tailored for handling chemical compositions. The class
42+
is immutable, hashable, and designed for robust usage in material science
43+
and chemistry computations.
44+
45+
Key Features:
46+
- Supports both `Element` and `Species` as keys, with differentiation
47+
between oxidation states (e.g., Fe2+ and Fe3+ are distinct keys).
48+
- Behaves like a dictionary but returns 0 for missing keys, making it
49+
similar to a `defaultdict` while remaining immutable.
50+
- Provides numerous utility methods for chemical computations, such as
51+
calculating fractions, weights, and formula representations.
52+
53+
Highlights:
54+
- **Input Flexibility**: Accepts formulas as strings, dictionaries, or
55+
keyword arguments for construction.
56+
- **Convenience Methods**: Includes `get_fraction`, `reduced_formula`,
57+
and weight-related utilities.
58+
- **Enhanced Formula Representation**: Supports reduced, normalized, and
59+
IUPAC-sorted formulas.
60+
61+
Examples:
62+
>>> comp = Composition("LiFePO4")
63+
>>> comp.get_atomic_fraction(Element("Li"))
64+
0.14285714285714285
65+
>>> comp.num_atoms
66+
7.0
67+
>>> comp.reduced_formula
68+
'LiFePO4'
69+
>>> comp.formula
70+
'Li1 Fe1 P1 O4'
71+
>>> comp.get_wt_fraction(Element("Li"))
72+
0.04399794666951898
73+
>>> comp.num_atoms
74+
7.0
75+
76+
Attributes:
77+
- `amount_tolerance` (float): Tolerance for distinguishing composition
78+
amounts. Default is 1e-8 to minimize floating-point errors.
79+
- `charge_balanced_tolerance` (float): Tolerance for verifying charge balance.
80+
- `special_formulas` (dict): Custom formula mappings for specific compounds
81+
(e.g., `"LiO"` → `"Li2O2"`).
82+
- `oxi_prob` (dict or None): Prior probabilities of oxidation states, used
83+
for oxidation state guessing.
84+
85+
Functionality:
86+
- Arithmetic Operations: Add, subtract, multiply, or divide compositions.
87+
For example:
88+
>>> comp1 = Composition("Fe2O3")
89+
>>> comp2 = Composition("FeO")
90+
>>> result = comp1 + comp2 # Produces "Fe3O4"
91+
- Representation:
92+
- `formula`: Full formula string with elements sorted by electronegativity.
93+
- `reduced_formula`: Simplified formula with minimal ratios.
94+
- `hill_formula`: Hill notation (C and H prioritized, others alphabetically sorted).
95+
- Utilities:
96+
- `get_atomic_fraction`: Returns the atomic fraction of a given element/species.
97+
- `get_wt_fraction`: Returns the weight fraction of a given element/species.
98+
- `is_element`: Checks if the composition is a pure element.
99+
- `reduced_composition`: Normalizes the composition by the greatest common denominator.
100+
- `fractional_composition`: Returns the normalized composition where sums equal 1.
101+
- Oxidation State Handling:
102+
- `oxi_state_guesses`: Suggests charge-balanced oxidation states.
103+
- `charge_balanced`: Checks if the composition is charge balanced.
104+
- `add_charges_from_oxi_state_guesses`: Assigns oxidation states based on guesses.
105+
- Validation:
106+
- `valid`: Ensures all elements/species are valid.
107+
108+
Notes:
109+
- When constructing from strings, both `Element` and `Species` types are
110+
handled. For example:
111+
- `Composition("Fe2+")` differentiates Fe2+ from Fe3+.
112+
- `Composition("Fe2O3")` auto-parses standard formulas.
74113
"""
75114

76115
# Tolerance in distinguishing different composition amounts.
@@ -547,7 +586,8 @@ def contains_element_type(self, category: str) -> bool:
547586

548587
return any(getattr(el, f"is_{category}") for el in self.elements)
549588

550-
def _parse_formula(self, formula: str, strict: bool = True) -> dict[str, float]:
589+
@staticmethod
590+
def _parse_formula(formula: str, strict: bool = True) -> dict[str, float]:
551591
"""
552592
Args:
553593
formula (str): A string formula, e.g. Fe2O3, Li3Fe2(PO4)3.
@@ -639,22 +679,64 @@ def from_dict(cls, dct: dict) -> Self:
639679
return cls(dct)
640680

641681
@classmethod
642-
def from_weight_dict(cls, weight_dict: dict[SpeciesLike, float]) -> Self:
682+
def from_weight_dict(cls, weight_dict: dict[SpeciesLike, float], strict: bool = True, **kwargs) -> Self:
643683
"""Create a Composition based on a dict of atomic fractions calculated
644684
from a dict of weight fractions. Allows for quick creation of the class
645685
from weight-based notations commonly used in the industry, such as
646686
Ti6V4Al and Ni60Ti40.
647687
648688
Args:
649689
weight_dict (dict): {symbol: weight_fraction} dict.
690+
strict (bool): Only allow valid Elements and Species in the Composition. Defaults to True.
691+
**kwargs: Additional kwargs supported by the dict() constructor.
650692
651693
Returns:
652-
Composition
694+
Composition in molar fractions.
695+
696+
Examples:
697+
>>> Composition.from_weights({"Fe": 0.5, "Ni": 0.5})
698+
Composition('Fe0.512434 Ni0.487566')
699+
>>> Composition.from_weights({"Ti": 60, "Ni": 40})
700+
Composition('Ti0.647796 Ni0.352204')
653701
"""
654702
weight_sum = sum(val / Element(el).atomic_mass for el, val in weight_dict.items())
655703
comp_dict = {el: val / Element(el).atomic_mass / weight_sum for el, val in weight_dict.items()}
656704

657-
return cls(comp_dict)
705+
return cls(comp_dict, strict=strict, **kwargs)
706+
707+
@classmethod
708+
def from_weights(cls, *args, strict: bool = True, **kwargs) -> Self:
709+
"""Create a Composition from a weight-based formula.
710+
711+
Args:
712+
*args: Any number of 2-tuples as key-value pairs.
713+
strict (bool): Only allow valid Elements and Species in the Composition. Defaults to False.
714+
allow_negative (bool): Whether to allow negative compositions. Defaults to False.
715+
**kwargs: Additional kwargs supported by the dict() constructor.
716+
717+
Returns:
718+
Composition in molar fractions.
719+
720+
Examples:
721+
>>> Composition.from_weights("Fe50Ti50")
722+
Composition('Fe0.461538 Ti0.538462')
723+
>>> Composition.from_weights({"Fe": 0.5, "Ni": 0.5})
724+
Composition('Fe0.512434 Ni0.487566')
725+
"""
726+
if len(args) == 1 and isinstance(args[0], str):
727+
elem_map: dict[str, float] = cls._parse_formula(args[0])
728+
elif len(args) == 1 and isinstance(args[0], type(cls)):
729+
elem_map = args[0] # type: ignore[assignment]
730+
elif len(args) == 1 and isinstance(args[0], float) and math.isnan(args[0]):
731+
raise ValueError("float('NaN') is not a valid Composition, did you mean 'NaN'?")
732+
else:
733+
elem_map = dict(*args, **kwargs) # type: ignore[assignment]
734+
735+
for val in elem_map.values():
736+
if val < -cls.amount_tolerance:
737+
raise ValueError("Weights in Composition cannot be negative!")
738+
739+
return cls.from_weight_dict(elem_map, strict=strict)
658740

659741
def get_el_amt_dict(self) -> dict[str, float]:
660742
"""

0 commit comments

Comments
 (0)