Skip to content

Commit

Permalink
Merge branch 'master' into test-monty-reverse-read
Browse files Browse the repository at this point in the history
  • Loading branch information
DanielYang59 authored Nov 25, 2024
2 parents a7bafa2 + 31f1e1f commit 9c0d003
Show file tree
Hide file tree
Showing 16 changed files with 681 additions and 448 deletions.
3 changes: 1 addition & 2 deletions src/pymatgen/analysis/magnetism/analyzer.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,8 +170,7 @@ def __init__(
has_spin = False
for comp in structure.species_and_occu:
for sp in comp:
if getattr(sp, "spin", False):
has_spin = True
has_spin |= bool(getattr(sp, "spin", False))

# perform input sanitation ...
# rest of class will assume magnetic moments are stored on site properties:
Expand Down
36 changes: 25 additions & 11 deletions src/pymatgen/analysis/xas/spectrum.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,16 @@
from scipy.interpolate import interp1d

from pymatgen.analysis.structure_matcher import StructureMatcher
from pymatgen.core import Element
from pymatgen.core.spectrum import Spectrum
from pymatgen.symmetry.analyzer import SpacegroupAnalyzer

if TYPE_CHECKING:
from collections.abc import Sequence
from typing import Literal

from pymatgen.core import Structure

__author__ = "Chen Zheng, Yiming Chen"
__copyright__ = "Copyright 2012, The Materials Project"
__version__ = "3.0"
Expand All @@ -42,29 +46,31 @@ class XAS(Spectrum):
Attributes:
x (Sequence[float]): The sequence of energies.
y (Sequence[float]): The sequence of mu(E).
absorbing_element (str): The absorbing element of the spectrum.
absorbing_element (str or .Element): The absorbing element of the spectrum.
edge (str): The edge of the spectrum.
spectrum_type (str): The type of the spectrum (XANES or EXAFS).
absorbing_index (int): The absorbing index of the spectrum.
zero_negative_intensity (bool) : Whether to set unphysical negative intensities to zero
"""

XLABEL = "Energy"
YLABEL = "Intensity"

def __init__(
self,
x,
y,
structure,
absorbing_element,
edge="K",
spectrum_type="XANES",
absorbing_index=None,
x: Sequence,
y: Sequence,
structure: Structure,
absorbing_element: str | Element,
edge: str = "K",
spectrum_type: str = "XANES",
absorbing_index: int | None = None,
zero_negative_intensity: bool = False,
):
"""Initialize a spectrum object."""
super().__init__(x, y, structure, absorbing_element, edge)
self.structure = structure
self.absorbing_element = absorbing_element
self.absorbing_element = Element(absorbing_element)
self.edge = edge
self.spectrum_type = spectrum_type
self.e0 = self.x[np.argmax(np.gradient(self.y) / np.gradient(self.x))]
Expand All @@ -75,8 +81,16 @@ def __init__(
]
self.absorbing_index = absorbing_index
# check for empty spectra and negative intensities
if sum(1 for i in self.y if i <= 0) / len(self.y) > 0.05:
raise ValueError("Double check the intensities. Most of them are non-positive.")
neg_intens_mask = self.y < 0.0
if len(self.y[neg_intens_mask]) / len(self.y) > 0.05:
warnings.warn(
"Double check the intensities. More than 5% of them are negative.",
UserWarning,
stacklevel=2,
)
self.zero_negative_intensity = zero_negative_intensity
if self.zero_negative_intensity:
self.y[neg_intens_mask] = 0.0

def __str__(self):
return (
Expand Down
160 changes: 121 additions & 39 deletions src/pymatgen/core/composition.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,41 +36,80 @@

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

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

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

def _parse_formula(self, formula: str, strict: bool = True) -> dict[str, float]:
@staticmethod
def _parse_formula(formula: str, strict: bool = True) -> dict[str, float]:
"""
Args:
formula (str): A string formula, e.g. Fe2O3, Li3Fe2(PO4)3.
Expand Down Expand Up @@ -639,22 +679,64 @@ def from_dict(cls, dct: dict) -> Self:
return cls(dct)

@classmethod
def from_weight_dict(cls, weight_dict: dict[SpeciesLike, float]) -> Self:
def from_weight_dict(cls, weight_dict: dict[SpeciesLike, float], strict: bool = True, **kwargs) -> Self:
"""Create a Composition based on a dict of atomic fractions calculated
from a dict of weight fractions. Allows for quick creation of the class
from weight-based notations commonly used in the industry, such as
Ti6V4Al and Ni60Ti40.
Args:
weight_dict (dict): {symbol: weight_fraction} dict.
strict (bool): Only allow valid Elements and Species in the Composition. Defaults to True.
**kwargs: Additional kwargs supported by the dict() constructor.
Returns:
Composition
Composition in molar fractions.
Examples:
>>> Composition.from_weights({"Fe": 0.5, "Ni": 0.5})
Composition('Fe0.512434 Ni0.487566')
>>> Composition.from_weights({"Ti": 60, "Ni": 40})
Composition('Ti0.647796 Ni0.352204')
"""
weight_sum = sum(val / Element(el).atomic_mass for el, val in weight_dict.items())
comp_dict = {el: val / Element(el).atomic_mass / weight_sum for el, val in weight_dict.items()}

return cls(comp_dict)
return cls(comp_dict, strict=strict, **kwargs)

@classmethod
def from_weights(cls, *args, strict: bool = True, **kwargs) -> Self:
"""Create a Composition from a weight-based formula.
Args:
*args: Any number of 2-tuples as key-value pairs.
strict (bool): Only allow valid Elements and Species in the Composition. Defaults to False.
allow_negative (bool): Whether to allow negative compositions. Defaults to False.
**kwargs: Additional kwargs supported by the dict() constructor.
Returns:
Composition in molar fractions.
Examples:
>>> Composition.from_weights("Fe50Ti50")
Composition('Fe0.461538 Ti0.538462')
>>> Composition.from_weights({"Fe": 0.5, "Ni": 0.5})
Composition('Fe0.512434 Ni0.487566')
"""
if len(args) == 1 and isinstance(args[0], str):
elem_map: dict[str, float] = cls._parse_formula(args[0])
elif len(args) == 1 and isinstance(args[0], type(cls)):
elem_map = args[0] # type: ignore[assignment]
elif len(args) == 1 and isinstance(args[0], float) and math.isnan(args[0]):
raise ValueError("float('NaN') is not a valid Composition, did you mean 'NaN'?")
else:
elem_map = dict(*args, **kwargs) # type: ignore[assignment]

for val in elem_map.values():
if val < -cls.amount_tolerance:
raise ValueError("Weights in Composition cannot be negative!")

return cls.from_weight_dict(elem_map, strict=strict)

def get_el_amt_dict(self) -> dict[str, float]:
"""
Expand Down
Loading

0 comments on commit 9c0d003

Please sign in to comment.