|
36 | 36 |
|
37 | 37 | @total_ordering
|
38 | 38 | 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. |
74 | 113 | """
|
75 | 114 |
|
76 | 115 | # Tolerance in distinguishing different composition amounts.
|
@@ -547,7 +586,8 @@ def contains_element_type(self, category: str) -> bool:
|
547 | 586 |
|
548 | 587 | return any(getattr(el, f"is_{category}") for el in self.elements)
|
549 | 588 |
|
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]: |
551 | 591 | """
|
552 | 592 | Args:
|
553 | 593 | formula (str): A string formula, e.g. Fe2O3, Li3Fe2(PO4)3.
|
@@ -639,22 +679,64 @@ def from_dict(cls, dct: dict) -> Self:
|
639 | 679 | return cls(dct)
|
640 | 680 |
|
641 | 681 | @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: |
643 | 683 | """Create a Composition based on a dict of atomic fractions calculated
|
644 | 684 | from a dict of weight fractions. Allows for quick creation of the class
|
645 | 685 | from weight-based notations commonly used in the industry, such as
|
646 | 686 | Ti6V4Al and Ni60Ti40.
|
647 | 687 |
|
648 | 688 | Args:
|
649 | 689 | 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. |
650 | 692 |
|
651 | 693 | 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') |
653 | 701 | """
|
654 | 702 | weight_sum = sum(val / Element(el).atomic_mass for el, val in weight_dict.items())
|
655 | 703 | comp_dict = {el: val / Element(el).atomic_mass / weight_sum for el, val in weight_dict.items()}
|
656 | 704 |
|
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) |
658 | 740 |
|
659 | 741 | def get_el_amt_dict(self) -> dict[str, float]:
|
660 | 742 | """
|
|
0 commit comments