12
12
import os
13
13
import re
14
14
import string
15
+ import warnings
15
16
from functools import total_ordering
16
17
from itertools import combinations_with_replacement , product
17
- from typing import Dict , List , Tuple , Union
18
+ from typing import Dict , Generator , List , Tuple , Union
18
19
19
20
from monty .fractions import gcd , gcd_float
20
21
from monty .json import MSONable
@@ -122,7 +123,7 @@ def __init__(self, *args, strict: bool = False, **kwargs):
122
123
if len (args ) == 1 and isinstance (args [0 ], Composition ):
123
124
elmap = args [0 ]
124
125
elif len (args ) == 1 and isinstance (args [0 ], str ):
125
- elmap = self ._parse_formula (args [0 ])
126
+ elmap = self ._parse_formula (args [0 ]) # type: ignore
126
127
else :
127
128
elmap = dict (* args , ** kwargs ) # type: ignore
128
129
elamt = {}
@@ -462,7 +463,8 @@ def __str__(self):
462
463
463
464
def to_pretty_string (self ) -> str :
464
465
"""
465
- :return: Same as __str__ but without spaces.
466
+ Returns:
467
+ str: Same as output __str__() but without spaces.
466
468
"""
467
469
return re .sub (r"\s+" , "" , self .__str__ ())
468
470
@@ -493,7 +495,7 @@ def get_atomic_fraction(self, el: SpeciesLike) -> float:
493
495
"""
494
496
return abs (self [el ]) / self ._natoms
495
497
496
- def get_wt_fraction (self , el : SpeciesLike ):
498
+ def get_wt_fraction (self , el : SpeciesLike ) -> float :
497
499
"""
498
500
Calculate weight fraction of an Element or Species.
499
501
@@ -505,10 +507,7 @@ def get_wt_fraction(self, el: SpeciesLike):
505
507
"""
506
508
return get_el_sp (el ).atomic_mass * abs (self [el ]) / self .weight
507
509
508
- def contains_element_type (
509
- self ,
510
- category : str ,
511
- ):
510
+ def contains_element_type (self , category : str ) -> bool :
512
511
"""
513
512
Check if Composition contains any elements matching a given category.
514
513
@@ -549,7 +548,7 @@ def contains_element_type(
549
548
return any (category [0 ] in el .block for el in self .elements )
550
549
return any (getattr (el , "is_{}" .format (category )) for el in self .elements )
551
550
552
- def _parse_formula (self , formula ) :
551
+ def _parse_formula (self , formula : str ) -> Dict [ str , float ] :
553
552
"""
554
553
Args:
555
554
formula (str): A string formula, e.g. Fe2O3, Li3Fe2(PO4)3
@@ -564,22 +563,22 @@ def _parse_formula(self, formula):
564
563
# for Metallofullerene like "Y3N@C80"
565
564
formula = formula .replace ("@" , "" )
566
565
567
- def get_sym_dict (f , factor ) :
568
- sym_dict = collections .defaultdict (float )
569
- for m in re .finditer (r"([A-Z][a-z]*)\s*([-*\.e\d]*)" , f ):
566
+ def get_sym_dict (form : str , factor : Union [ int , float ]) -> Dict [ str , float ] :
567
+ sym_dict : Dict [ str , float ] = collections .defaultdict (float )
568
+ for m in re .finditer (r"([A-Z][a-z]*)\s*([-*\.e\d]*)" , form ):
570
569
el = m .group (1 )
571
- amt = 1
570
+ amt = 1.0
572
571
if m .group (2 ).strip () != "" :
573
572
amt = float (m .group (2 ))
574
573
sym_dict [el ] += amt * factor
575
- f = f .replace (m .group (), "" , 1 )
576
- if f .strip ():
577
- raise ValueError ("{} is an invalid formula!" .format (f ))
574
+ form = form .replace (m .group (), "" , 1 )
575
+ if form .strip ():
576
+ raise ValueError ("{} is an invalid formula!" .format (form ))
578
577
return sym_dict
579
578
580
579
m = re .search (r"\(([^\(\)]+)\)\s*([\.e\d]*)" , formula )
581
580
if m :
582
- factor = 1
581
+ factor = 1.0
583
582
if m .group (2 ) != "" :
584
583
factor = float (m .group (2 ))
585
584
unit_sym_dict = get_sym_dict (m .group (1 ), factor )
@@ -619,7 +618,7 @@ def chemical_system(self) -> str:
619
618
sorted alphabetically and joined by dashes, by convention for use
620
619
in database keys.
621
620
"""
622
- return "-" .join (sorted ([ el .symbol for el in self .elements ] ))
621
+ return "-" .join (sorted (el .symbol for el in self .elements ))
623
622
624
623
@property
625
624
def valid (self ) -> bool :
@@ -629,7 +628,7 @@ def valid(self) -> bool:
629
628
"""
630
629
return not any (isinstance (el , DummySpecies ) for el in self .elements )
631
630
632
- def __repr__ (self ):
631
+ def __repr__ (self ) -> str :
633
632
return "Comp: " + self .formula
634
633
635
634
@classmethod
@@ -657,7 +656,7 @@ def get_el_amt_dict(self) -> Dict[str, float]:
657
656
d [e .symbol ] += a
658
657
return d
659
658
660
- def as_dict (self ) -> dict :
659
+ def as_dict (self ) -> Dict [ str , float ] :
661
660
"""
662
661
Returns:
663
662
dict with species symbol and (unreduced) amount e.g.,
@@ -735,6 +734,42 @@ def oxi_state_guesses(
735
734
736
735
return self ._get_oxid_state_guesses (all_oxi_states , max_sites , oxi_states_override , target_charge )[0 ]
737
736
737
+ def replace (self , elem_map : Dict [str , Union [str , Dict [str , Union [int , float ]]]]) -> "Composition" :
738
+ """
739
+ Replace elements in a composition. Returns a new Composition, leaving the old one unchanged.
740
+
741
+ Args:
742
+ elem_map (dict[str, str | dict[str, int | float]]): dict of elements or species to swap. E.g.
743
+ {"Li": "Na"} performs a Li for Na substitution. The target can be a {species: factor} dict. For
744
+ example, in Fe2O3 you could map {"Fe": {"Mg": 0.5, "Cu":0.5}} to obtain MgCuO3.
745
+
746
+ Returns:
747
+ Composition: New object with elements remapped according to elem_map.
748
+ """
749
+
750
+ # drop inapplicable substitutions
751
+ invalid_elems = [key for key in elem_map if key not in self ]
752
+ if invalid_elems :
753
+ warnings .warn (
754
+ "Some elements to be substituted are not present in composition. Please check your input. "
755
+ f"Problematic element = { invalid_elems } ; { self } "
756
+ )
757
+ for elem in invalid_elems :
758
+ elem_map .pop (elem )
759
+
760
+ new_comp = self .as_dict ()
761
+
762
+ for old_elem , new_elem in elem_map .items ():
763
+ amount = new_comp .pop (old_elem )
764
+
765
+ if isinstance (new_elem , dict ):
766
+ for el , factor in new_elem .items ():
767
+ new_comp [el ] = factor * amount
768
+ else :
769
+ new_comp [new_elem ] = amount
770
+
771
+ return Composition (new_comp )
772
+
738
773
def add_charges_from_oxi_state_guesses (
739
774
self ,
740
775
oxi_states_override : dict = None ,
@@ -938,7 +973,9 @@ def _get_oxid_state_guesses(self, all_oxi_states, max_sites, oxi_states_override
938
973
return all_sols , all_oxid_combo
939
974
940
975
@staticmethod
941
- def ranked_compositions_from_indeterminate_formula (fuzzy_formula , lock_if_strict = True ):
976
+ def ranked_compositions_from_indeterminate_formula (
977
+ fuzzy_formula : str , lock_if_strict : bool = True
978
+ ) -> List ["Composition" ]:
942
979
"""
943
980
Takes in a formula where capitalization might not be correctly entered,
944
981
and suggests a ranked list of potential Composition matches.
@@ -969,14 +1006,19 @@ def ranked_compositions_from_indeterminate_formula(fuzzy_formula, lock_if_strict
969
1006
970
1007
all_matches = Composition ._comps_from_fuzzy_formula (fuzzy_formula )
971
1008
# remove duplicates
972
- all_matches = list (set (all_matches ))
1009
+ uniq_matches = list (set (all_matches ))
973
1010
# sort matches by rank descending
974
- all_matches = sorted (all_matches , key = lambda match : (match [1 ], match [0 ]), reverse = True )
975
- all_matches = [ m [ 0 ] for m in all_matches ]
976
- return all_matches
1011
+ ranked_matches = sorted (uniq_matches , key = lambda match : (match [1 ], match [0 ]), reverse = True )
1012
+
1013
+ return [ m [ 0 ] for m in ranked_matches ]
977
1014
978
1015
@staticmethod
979
- def _comps_from_fuzzy_formula (fuzzy_formula , m_dict = None , m_points = 0 , factor = 1 ):
1016
+ def _comps_from_fuzzy_formula (
1017
+ fuzzy_formula : str ,
1018
+ m_dict : Dict [str , float ] = None ,
1019
+ m_points : int = 0 ,
1020
+ factor : Union [int , float ] = 1 ,
1021
+ ) -> Generator [Tuple ["Composition" , int ], None , None ]:
980
1022
"""
981
1023
A recursive helper method for formula parsing that helps in
982
1024
interpreting and ranking indeterminate formulas.
@@ -993,9 +1035,8 @@ def _comps_from_fuzzy_formula(fuzzy_formula, m_dict=None, m_points=0, factor=1):
993
1035
as the fuzzy_formula with a coefficient of 2.
994
1036
995
1037
Returns:
996
- A list of tuples, with the first element being a Composition and
997
- the second element being the number of points awarded that
998
- Composition intepretation.
1038
+ list[tuple[Composition, int]]: A list of tuples, with the first element being a Composition
1039
+ and the second element being the number of points awarded that Composition interpretation.
999
1040
"""
1000
1041
m_dict = m_dict or {}
1001
1042
@@ -1126,7 +1167,7 @@ def reduce_formula(sym_amt, iupac_ordering: bool = False) -> Tuple[str, float]:
1126
1167
Table VI of "Nomenclature of Inorganic Chemistry (IUPAC
1127
1168
Recommendations 2005)". This ordering effectively follows
1128
1169
the groups and rows of the periodic table, except the
1129
- Lanthanides, Actanides and hydrogen. Note that polyanions
1170
+ Lanthanides, Actinides and hydrogen. Note that polyanions
1130
1171
will still be determined based on the true electronegativity of
1131
1172
the elements.
1132
1173
@@ -1168,10 +1209,8 @@ def reduce_formula(sym_amt, iupac_ordering: bool = False) -> Tuple[str, float]:
1168
1209
1169
1210
class ChemicalPotential (dict , MSONable ):
1170
1211
"""
1171
- Class to represent set of chemical potentials. Can be:
1172
- multiplied/divided by a Number
1173
- multiplied by a Composition (returns an energy)
1174
- added/subtracted with other ChemicalPotentials.
1212
+ Class to represent set of chemical potentials. Can be: multiplied/divided by a Number
1213
+ multiplied by a Composition (returns an energy) added/subtracted with other ChemicalPotentials.
1175
1214
"""
1176
1215
1177
1216
def __init__ (self , * args , ** kwargs ):
0 commit comments