Skip to content

Commit b180cac

Browse files
committed
Merge remote-tracking branch 'upstream/master' into str-fixes
2 parents a632d7c + acfe589 commit b180cac

File tree

5 files changed

+128
-69
lines changed

5 files changed

+128
-69
lines changed

ADMIN.rst

+13-13
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
Introduction
22
============
33

4-
This docmentation provides a guide for pymatgen administrators. The following
4+
This documentation provides a guide for pymatgen administrators. The following
55
assumes you are using miniconda or Anaconda.
66

77
Releases
@@ -23,18 +23,18 @@ Install some conda tools first::
2323
conda install --yes conda-build anaconda-client
2424
conda config --add channels matsci
2525

26-
Pymatgen uses `invoke <http://www.pyinvoke.org/>`_ to automate releases. You will
26+
Pymatgen uses `invoke <http://www.pyinvoke.org/>`_ to automate releases. You will
2727
also need sphinx and doc2dash. Install these using::
2828

2929
pip install --upgrade invoke sphinx doc2dash
3030

31-
For 2018, we will release both py27 and py37 versions of pymatgen. Create
31+
For 2018, we will release both py27 and py37 versions of pymatgen. Create
3232
environments for py27 and py37 using conda::
3333

3434
conda create --yes -n py37 python=3.7
3535
conda create --yes -n py27 python=2.7
3636

37-
For each env, install some packages using conda followed by dev install for
37+
For each env, install some packages using conda followed by dev install for
3838
pymatgen::
3939

4040
conda activate py37
@@ -50,43 +50,43 @@ pymatgen::
5050
pip install invoke sphinx doc2dash
5151
python setup.py develop
5252

53-
Add your PyPI username and password and GITHUB_RELEASE_TOKEN into your
53+
Add your PyPI username and password and GITHUB_RELEASE_TOKEN into your
5454
environment::
5555

5656
export TWINE_USERNAME=PYPIUSERNAME
5757
export TWINE_PASSWORD=PYPIPASSWORD
5858
export GITHUB_RELEASES_TOKEN=TOKEN_YOU_GET_FROM_GITHUB
5959

60-
You may want to add these to your .bash_profile to avoid having to type these
60+
You may want to add these to your .bash_profile to avoid having to type these
6161
each time.
6262

6363
Machine-specific issues
6464
~~~~~~~~~~~~~~~~~~~~~~~
6565

66-
The above instructions are general, but there are some known issues that are
66+
The above instructions are general, but there are some known issues that are
6767
machine-specific:
6868

69-
* Installing lxml via pip required `STATIC_DEPS=true pip install lxml` on
69+
* Installing lxml via pip required `STATIC_DEPS=true pip install lxml` on
7070
macOS 10.13.
71-
* It can be useful to `pip install --upgrade pip twine setuptools` (this may
71+
* It can be useful to `pip install --upgrade pip twine setuptools` (this may
7272
be necessary if there are authentication errors when connecting to PyPI).
7373
* You may have to `brew install hdf5 netcdf` or similar to be able to pip
7474
install the optional requirement `netCDF4`.
7575

7676
Doing the release
7777
-----------------
7878

79-
Ensure appropriate environment variabels are set including `DISCOURSE_API_USERNAME`,
79+
Ensure appropriate environment variables are set including `DISCOURSE_API_USERNAME`,
8080
`DISCOURSE_API_KEY` and `GITHUB_RELEASES_TOKEN`.
8181

82-
First update the change log. The autogenerated change log is simply a list of
82+
First update the change log. The autogenerated change log is simply a list of
8383
commit messages since the last version. Make sure to edit the log for brevity
8484
and to attribute significant features to appropriate developers::
8585

8686
conda activate py37
8787
invoke update-changelog
8888

89-
Then, do the release with the following sequence of commands (you can put them
89+
Then, do the release with the following sequence of commands (you can put them
9090
in a bash script in your PATH somewhere)::
9191

9292
conda activate py37
@@ -97,7 +97,7 @@ in a bash script in your PATH somewhere)::
9797
python setup.py develop
9898

9999
Double check that the releases are properly done on Pypi. If you are releasing
100-
on a Mac, you should see a pymatgen.version.tar.gz and two wheels (Py37 and
100+
on a Mac, you should see a pymatgen.version.tar.gz and two wheels (Py37 and
101101
P). There will be a py37 wheel for Windows that is generated by Appveyor.
102102

103103
Materials.sh

CONTRIBUTING.rst

+10-10
Original file line numberDiff line numberDiff line change
@@ -76,9 +76,9 @@ http://www.eqqon.com/index.php/Collaborative_Github_Workflow):
7676
pymatgen maintainers. They will pull your commits and run their own tests
7777
before releasing.
7878

79-
"Work-in-progress" pull requests are encouraged, especially if this is your
80-
first time contributing to pymatgen, and the maintainers will be happy to
81-
help or provide code review as necessary. Put "[WIP]" in the title of your
79+
"Work-in-progress" pull requests are encouraged, especially if this is your
80+
first time contributing to pymatgen, and the maintainers will be happy to
81+
help or provide code review as necessary. Put "[WIP]" in the title of your
8282
pull request to indicate it's not ready to be merged.
8383

8484
Coding Guidelines
@@ -88,7 +88,7 @@ Given that pymatgen is intended to be long-term code base, we adopt very strict
8888
quality control and coding guidelines for all contributions to pymatgen. The
8989
following must be satisfied for your contributions to be accepted into pymatgen.
9090

91-
1. **Unittests** are required for all new modules and methods. The only way to
91+
1. **Unit tests** are required for all new modules and methods. The only way to
9292
minimize code regression is to ensure that all code are well-tested. If the
9393
maintainer cannot test your code, the contribution will be rejected.
9494
2. **Python PEP 8** `code style <http://www.python.org/dev/peps/pep-0008/>`_.
@@ -101,9 +101,9 @@ following must be satisfied for your contributions to be accepted into pymatgen.
101101
prior to any commits. At the very least, copy pre-commit to .git/hooks/pre-push.
102102
3. **Python 3**. We only support Python 3.7+.
103103
4. **Documentation** required for all modules, classes and methods. In
104-
particular, the method docstrings should make clear the arguments expected
104+
particular, the method doc strings should make clear the arguments expected
105105
and the return values. For complex algorithms (e.g., an Ewald summation), a
106-
summary of the alogirthm should be provided, and preferably with a link to a
106+
summary of the algorithm should be provided, and preferably with a link to a
107107
publication outlining the method in detail.
108108
5. **IDE**. We highly recommend the use of Pycharm. You should also set up
109109
pycodestyle and turn those on within the IDE setup. This will warn of any
@@ -116,10 +116,10 @@ examples of what is expected.
116116
A word on coding for Python 2 compatibility
117117
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
118118

119-
As of 2019, pymatgen no longer requires code to be Python 2 compatible, and
120-
current versions of the code are not supported with Python 2. If you need a
121-
version of pymatgen that works with Python 2, please use a version before
122-
2018, but note this will be missing the latest bug fixes. This change follows
119+
As of 2019, pymatgen no longer requires code to be Python 2 compatible, and
120+
current versions of the code are not supported with Python 2. If you need a
121+
version of pymatgen that works with Python 2, please use a version before
122+
2018, but note this will be missing the latest bug fixes. This change follows
123123
the broader Python community no longer supporting Python 2, including numpy.
124124

125125
.. _`pymatgen's Google Groups page`: https://groups.google.com/forum/?fromgroups#!forum/pymatgen/

README.rst

+1-1
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ Why use pymatgen?
7777
1. **It is (fairly) robust.** Pymatgen is used by thousands of researchers, and is the analysis code powering the
7878
`Materials Project`_. The analysis it produces survives rigorous scrutiny every single day. Bugs tend to be
7979
found and corrected quickly. Pymatgen also uses Github Actions for continuous integration, which ensures that every
80-
new code passes a comprehensive suite of unittests.
80+
new code passes a comprehensive suite of unit tests.
8181
2. **It is well documented.** A fairly comprehensive documentation has been written to help you get to grips with it
8282
quickly.
8383
3. **It is open.** You are free to use and contribute to pymatgen. It also means that pymatgen is continuously being

pymatgen/core/composition.py

+73-34
Original file line numberDiff line numberDiff line change
@@ -12,9 +12,10 @@
1212
import os
1313
import re
1414
import string
15+
import warnings
1516
from functools import total_ordering
1617
from itertools import combinations_with_replacement, product
17-
from typing import Dict, List, Tuple, Union
18+
from typing import Dict, Generator, List, Tuple, Union
1819

1920
from monty.fractions import gcd, gcd_float
2021
from monty.json import MSONable
@@ -122,7 +123,7 @@ def __init__(self, *args, strict: bool = False, **kwargs):
122123
if len(args) == 1 and isinstance(args[0], Composition):
123124
elmap = args[0]
124125
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
126127
else:
127128
elmap = dict(*args, **kwargs) # type: ignore
128129
elamt = {}
@@ -462,7 +463,8 @@ def __str__(self):
462463

463464
def to_pretty_string(self) -> str:
464465
"""
465-
:return: Same as __str__ but without spaces.
466+
Returns:
467+
str: Same as output __str__() but without spaces.
466468
"""
467469
return re.sub(r"\s+", "", self.__str__())
468470

@@ -493,7 +495,7 @@ def get_atomic_fraction(self, el: SpeciesLike) -> float:
493495
"""
494496
return abs(self[el]) / self._natoms
495497

496-
def get_wt_fraction(self, el: SpeciesLike):
498+
def get_wt_fraction(self, el: SpeciesLike) -> float:
497499
"""
498500
Calculate weight fraction of an Element or Species.
499501
@@ -505,10 +507,7 @@ def get_wt_fraction(self, el: SpeciesLike):
505507
"""
506508
return get_el_sp(el).atomic_mass * abs(self[el]) / self.weight
507509

508-
def contains_element_type(
509-
self,
510-
category: str,
511-
):
510+
def contains_element_type(self, category: str) -> bool:
512511
"""
513512
Check if Composition contains any elements matching a given category.
514513
@@ -549,7 +548,7 @@ def contains_element_type(
549548
return any(category[0] in el.block for el in self.elements)
550549
return any(getattr(el, "is_{}".format(category)) for el in self.elements)
551550

552-
def _parse_formula(self, formula):
551+
def _parse_formula(self, formula: str) -> Dict[str, float]:
553552
"""
554553
Args:
555554
formula (str): A string formula, e.g. Fe2O3, Li3Fe2(PO4)3
@@ -564,22 +563,22 @@ def _parse_formula(self, formula):
564563
# for Metallofullerene like "Y3N@C80"
565564
formula = formula.replace("@", "")
566565

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):
570569
el = m.group(1)
571-
amt = 1
570+
amt = 1.0
572571
if m.group(2).strip() != "":
573572
amt = float(m.group(2))
574573
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))
578577
return sym_dict
579578

580579
m = re.search(r"\(([^\(\)]+)\)\s*([\.e\d]*)", formula)
581580
if m:
582-
factor = 1
581+
factor = 1.0
583582
if m.group(2) != "":
584583
factor = float(m.group(2))
585584
unit_sym_dict = get_sym_dict(m.group(1), factor)
@@ -619,7 +618,7 @@ def chemical_system(self) -> str:
619618
sorted alphabetically and joined by dashes, by convention for use
620619
in database keys.
621620
"""
622-
return "-".join(sorted([el.symbol for el in self.elements]))
621+
return "-".join(sorted(el.symbol for el in self.elements))
623622

624623
@property
625624
def valid(self) -> bool:
@@ -629,7 +628,7 @@ def valid(self) -> bool:
629628
"""
630629
return not any(isinstance(el, DummySpecies) for el in self.elements)
631630

632-
def __repr__(self):
631+
def __repr__(self) -> str:
633632
return "Comp: " + self.formula
634633

635634
@classmethod
@@ -657,7 +656,7 @@ def get_el_amt_dict(self) -> Dict[str, float]:
657656
d[e.symbol] += a
658657
return d
659658

660-
def as_dict(self) -> dict:
659+
def as_dict(self) -> Dict[str, float]:
661660
"""
662661
Returns:
663662
dict with species symbol and (unreduced) amount e.g.,
@@ -735,6 +734,42 @@ def oxi_state_guesses(
735734

736735
return self._get_oxid_state_guesses(all_oxi_states, max_sites, oxi_states_override, target_charge)[0]
737736

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+
738773
def add_charges_from_oxi_state_guesses(
739774
self,
740775
oxi_states_override: dict = None,
@@ -938,7 +973,9 @@ def _get_oxid_state_guesses(self, all_oxi_states, max_sites, oxi_states_override
938973
return all_sols, all_oxid_combo
939974

940975
@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"]:
942979
"""
943980
Takes in a formula where capitalization might not be correctly entered,
944981
and suggests a ranked list of potential Composition matches.
@@ -969,14 +1006,19 @@ def ranked_compositions_from_indeterminate_formula(fuzzy_formula, lock_if_strict
9691006

9701007
all_matches = Composition._comps_from_fuzzy_formula(fuzzy_formula)
9711008
# remove duplicates
972-
all_matches = list(set(all_matches))
1009+
uniq_matches = list(set(all_matches))
9731010
# 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]
9771014

9781015
@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]:
9801022
"""
9811023
A recursive helper method for formula parsing that helps in
9821024
interpreting and ranking indeterminate formulas.
@@ -993,9 +1035,8 @@ def _comps_from_fuzzy_formula(fuzzy_formula, m_dict=None, m_points=0, factor=1):
9931035
as the fuzzy_formula with a coefficient of 2.
9941036
9951037
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.
9991040
"""
10001041
m_dict = m_dict or {}
10011042

@@ -1126,7 +1167,7 @@ def reduce_formula(sym_amt, iupac_ordering: bool = False) -> Tuple[str, float]:
11261167
Table VI of "Nomenclature of Inorganic Chemistry (IUPAC
11271168
Recommendations 2005)". This ordering effectively follows
11281169
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
11301171
will still be determined based on the true electronegativity of
11311172
the elements.
11321173
@@ -1168,10 +1209,8 @@ def reduce_formula(sym_amt, iupac_ordering: bool = False) -> Tuple[str, float]:
11681209

11691210
class ChemicalPotential(dict, MSONable):
11701211
"""
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.
11751214
"""
11761215

11771216
def __init__(self, *args, **kwargs):

0 commit comments

Comments
 (0)