Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 12 additions & 10 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
# branches: [ master ]
release:
types: [published]

Expand All @@ -22,7 +22,7 @@ jobs:

- name: Build the wheel
run: |
python -m pip install build
python -m pip install --group dev
python -m build

- name: Upload wheel
Expand All @@ -33,13 +33,10 @@ jobs:

- name: Run the tests
run: |
python -m pip install numpy pyparsing pytest pytest-cov
pytest -v

- name: Build the docs
run: |
python -m pip install matplotlib sphinx
python -m pip install dist/periodictable*.whl
make -j 4 -C doc/sphinx SPHINXOPTS="-W --keep-going" html

# Test the wheel on different platforms
Expand Down Expand Up @@ -75,17 +72,22 @@ jobs:
run: python -m pip install dist/periodictable*.whl
shell: bash

- name: Install Python dependencies
# uncertainties and matplotlib are optional packages, but they are needed for mypy tests
- name: Install test dependencies
run: |
python -m pip install pytest pytest-cov
python -m pip install --group test

# Use --pyargs --import-mode=append to target the installed package.
# Change into the test directory to test the wheel so that the
# source directory is not on the path. Full tests with coverage are
# run before building the wheel.
# source directory is not on the path. We are running coverage tests
# because it shows that it is indeed the installed package that is tested.
# The mypy plugin fails when testing the installed package. Since there is no
# way to turn the option off, so we instead erase the addopts configuration
# and put them explicitly on the pytest command line below without --mypy.
- name: Test wheel with pytest
run: |
cd test
pytest -v --pyargs --import-mode=append periodictable . ../doc/sphinx/guide
pytest -v --pyargs --import-mode=append periodictable --override-ini addopts= --doctest-modules --doctest-glob=*.rst --cov=periodictable . ../doc/sphinx/guide

# Upload wheel to PyPI only when a tag is pushed, and its name begins with 'v'
upload-to-pypi:
Expand Down
1 change: 1 addition & 0 deletions MANIFEST.in
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ include README.rst
graft periodictable/xsf
include periodictable/f0_WaasKirf.dat
include periodictable/activation.dat
include periodictable/py.typed
prune doc
prune doc/sphinx/_build
prune doc/sphinx/build
Expand Down
3 changes: 3 additions & 0 deletions periodictable/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ def __dir__():
livermorium = Lv = elements.symbol("Lv")
tennessine = Ts = elements.symbol("Ts")
oganesson = Og = elements.symbol("Og")
deuterium = D = elements.symbol("D")
tritium = T = elements.symbol("T")
neutron = n = elements.symbol("n")

# Add element name and symbol (e.g. nickel and Ni) to the public attributes.
__all__ += core.define_elements(elements, globals())
Expand Down
6 changes: 3 additions & 3 deletions periodictable/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -651,7 +651,7 @@ def __init__(self, element: Union["Element", "Isotope"], charge: int):
def __getattr__(self, attr: str) -> Any:
return getattr(self.element, attr)
@property
def mass(self) -> float:
def mass(self) -> float: # type: ignore[override]
return getattr(self.element, 'mass') - constants.electron_mass*self.charge
def __str__(self) -> str:
sign = '+' if self.charge > 0 else '-'
Expand All @@ -661,12 +661,12 @@ def __str__(self) -> str:
def __repr__(self) -> str:
return repr(self.element)+'.ion[%d]'%self.charge
def __reduce__(self):
try:
if isinstance(self.element, Isotope):
return _make_isotope_ion, (self.element.table,
self.element.number,
self.element.isotope,
self.charge)
except Exception:
else:
return _make_ion, (self.element.table,
self.element.number,
self.charge)
Expand Down
3 changes: 2 additions & 1 deletion periodictable/crystal_structure.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@
This data is from Ashcroft and Mermin.
'''

from typing import Any
from .core import PeriodicTable

crystal_structures = [
crystal_structures: list[dict[str, Any]|None] = [
{'symmetry': 'diatom', 'd': 0.74}, #H
{'symmetry': 'atom'}, #He
{'symmetry': 'BCC', 'a': 3.49}, #Li
Expand Down
8 changes: 4 additions & 4 deletions periodictable/density.py
Original file line number Diff line number Diff line change
Expand Up @@ -143,18 +143,18 @@ def init(table: PeriodicTable, reload: bool=False) -> None:
return
table.properties.append('density')
Isotope.density \
= property(density, "density using inter-atomic spacing from naturally occurring form")
= property(density, doc="density using inter-atomic spacing from naturally occurring form") # type: ignore[assignment]
Element.density \
= property(density, "density using inter-atomic spacing from naturally occurring form")
= property(density, doc="density using inter-atomic spacing from naturally occurring form") # type: ignore[assignment]
Element.density_units = "g/cm^3"

Element.interatomic_distance \
= property(interatomic_distance,
"interatomic distance estimated from density")
doc="interatomic distance estimated from density") # type: ignore[assignment]
Element.interatomic_distance_units = "angstrom"
Element.number_density \
= property(number_density,
"number density estimated from mass and density")
doc="number density estimated from mass and density") # type: ignore[assignment]
Element.number_density_units = "1/cm^3"

for k, v in element_densities.items():
Expand Down
9 changes: 5 additions & 4 deletions periodictable/fasta.py
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,7 @@
from pathlib import Path
# Warning: name clash with Sequence
from collections.abc import Iterator
from typing import IO
from typing import IO, cast

from .formulas import formula as parse_formula, Formula, FormulaInput
from .nsf import neutron_sld
Expand All @@ -82,7 +82,7 @@
from .constants import avogadro_number

# CRUFT 1.5.2: retaining fasta.isotope_substitution for compatibility
def isotope_substitution(formula: Formula, source: Atom, target: Atom, portion: float=1):
def isotope_substitution(formula: Formula, source: Atom, target: Atom, portion: float=1.0):
"""
Substitute one atom/isotope in a formula with another in some proportion.

Expand Down Expand Up @@ -188,6 +188,7 @@ def __init__(
M.density = 1e24*M.molecular_mass/cell_volume if cell_volume > 0 else 0
#print name, M.molecular_mass, cell_volume, M.density
else:
assert M.density is not None, "Need density to compute cell volume"
cell_volume = 1e24*M.molecular_mass/M.density

H = M.replace(elements.H[1], elements.H)
Expand Down Expand Up @@ -590,9 +591,9 @@ def fasta_table() -> None:
for v in rows:
protons = sum(num*el.number for el, num in v.natural_formula.atoms.items())
electrons = protons - v.charge
Xsld = xray_sld(v.formula, wavelength=elements.Cu.K_alpha)
Xsld = xray_sld(v.formula, wavelength=cast(float, elements.Cu.K_alpha))
print("%25s %7.1f %7.1f %7.1f %5.2f %5d %5.2f %5.2f %5.2f %5.1f"%(
v.name, v.mass, v.Dmass, v.cell_volume, v.natural_formula.density,
v.name, v.mass, v.Dmass, v.cell_volume, v.natural_formula.density or 0.,
electrons, Xsld[0], v.sld, v.Dsld, v.D2Omatch))

beta_casein = "RELEELNVPGEIVESLSSSEESITRINKKIEKFQSEEQQQTEDELQDKIHPFAQTQSLVYPFPGPIPNSLPQNIPPLTQTPVVVPPFLQPEVMGVSKVKEAMAPKHKEMPFPKYPVEPFTESQSLTLTDVENLHLPLPLLQSWMHQPHQPLPPTVMFPPQSVLSLSQSKVLPVPQKAVPYPQRDMPIQAFLLYQEPVLGPVRGPFPIIV"
Expand Down
25 changes: 11 additions & 14 deletions periodictable/formulas.py
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,7 @@ def _mix_by_weight_pairs(pairs: list[tuple["Formula", float]]) -> "Formula":
result += ((q/f.mass)/scale) * f
if all(f.density for f, _ in pairs):
# Tested that densities are not None, so the following will work
volume = sum(q/f.density for f, q in pairs)/scale
volume = sum(q/cast(float, f.density) for f, q in pairs)/scale
result.density = result.mass/volume
return result

Expand Down Expand Up @@ -193,9 +193,9 @@ def _mix_by_volume_pairs(pairs: list[tuple["Formula", float]]) -> "Formula":
# = q / (mass/density)
# = q * density / mass
# scale this so that n = 1 for the smallest quantity
scale = min(q*f.density/f.mass for f, q in pairs)
scale = min(q*cast(float, f.density)/f.mass for f, q in pairs)
for f, q in pairs:
result += ((q*f.density/f.mass)/scale) * f
result += ((q*cast(float, f.density)/f.mass)/scale) * f

volume = sum(q for _, q in pairs)/scale
result.density = result.mass/volume
Expand Down Expand Up @@ -298,7 +298,7 @@ def formula(
structure = ((1, cast(Atom, compound)), )
elif isinstance(compound, dict):
structure = _convert_to_hill_notation(cast(dict[Atom, float], compound))
elif _is_string_like(compound):
elif isinstance(compound, str):
try:
chem = parse_formula(compound, table=table)
if name:
Expand Down Expand Up @@ -403,14 +403,16 @@ def natural_mass_ratio(self) -> float:
return (total_natural_mass + charge_correction)/(total_isotope_mass + charge_correction)

@property
def natural_density(self) -> float:
def natural_density(self) -> float | None:
"""
|g/cm^3|

Density of the formula with specific isotopes of each element
replaced by the naturally occurring abundance of the element
without changing the cell volume.
"""
if self.density is None:
return None
return self.density*self.natural_mass_ratio()

@natural_density.setter
Expand Down Expand Up @@ -455,7 +457,7 @@ def mass_fraction(self) -> dict[Atom, float]:
return dict((a, m*a.mass/total_mass) for a, m in self.atoms.items())

# TODO: Remove compound._pf. It is unused and wrong.
def _pf(self) -> float:
def _pf(self) -> float | None:
"""
packing factor | unitless

Expand Down Expand Up @@ -531,6 +533,7 @@ def volume(self, *args, **kw) -> float:
# Compute atomic volume
V = 0.
for atom, count in self.atoms.items():
assert atom.covalent_radius is not None, "Need covalent radius for "+str(atom)
radius = atom.covalent_radius
#if el.number == 1 and H_radius is not None:
# radius = H_radius
Expand Down Expand Up @@ -607,7 +610,7 @@ def change_table(self, table: PeriodicTable) -> "Formula":
self.structure = _change_table(self.structure, table)
return self

def replace(self, source, target, portion=1):
def replace(self, source, target, portion=1.0):
"""
Create a new formula with one atom/isotope substituted for another.

Expand Down Expand Up @@ -688,6 +691,7 @@ def _isotope_substitution(compound: "Formula", source: Atom, target: Atom, porti
"""
# TODO: fails if density is not defined
atoms = compound.atoms
assert compound.density is not None, "Need density for isotope substitution"
if source in atoms:
mass = compound.mass
mass_reduction = atoms[source]*portion*(source.mass - target.mass)
Expand Down Expand Up @@ -1099,13 +1103,6 @@ def _str_atoms(seq) -> str:

return ret

def _is_string_like(val: Any) -> bool:
"""Returns True if val acts like a string"""
try:
val+''
except Exception:
return False
return True

def from_subscript(value: str) -> str:
"""
Expand Down
23 changes: 12 additions & 11 deletions periodictable/mass.py
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@
materials (IUPAC Technical Report). Pure and Applied Chemistry, 93(1), 155-166.
https://doi.org/10.1515/pac-2018-0916
"""
from typing import cast
from .core import Element, Isotope, PeriodicTable, default_table
from .util import parse_uncertainty

Expand Down Expand Up @@ -94,10 +95,10 @@ def init(table: PeriodicTable, reload: bool=False) -> None:
if 'mass' in table.properties and not reload:
return
table.properties.append('mass')
Element.mass = property(mass, doc=mass.__doc__)
Element.mass = property(mass, doc=mass.__doc__) # type: ignore[assignment]
Element.mass_units = "u"
Isotope.mass = property(mass, doc=mass.__doc__)
Isotope.abundance = property(abundance, doc=abundance.__doc__)
Isotope.mass = property(mass, doc=mass.__doc__) # type: ignore[assignment]
Isotope.abundance = property(abundance, doc=abundance.__doc__) # type: ignore[assignment]
Isotope.abundance_units = "%"

# Parse isotope mass table where each line looks like:
Expand All @@ -113,9 +114,9 @@ def init(table: PeriodicTable, reload: bool=False) -> None:
iso = el.add_isotope(int(astr))
# Note: new mass table doesn't include nominal values for transuranics
# so use old masses here and override later with new masses.
el._mass, el._mass_unc = parse_uncertainty(el_mass)
el._mass, el._mass_unc = cast(tuple[float, float], parse_uncertainty(el_mass))
#el._mass, el._mass_unc = None, None
iso._mass, iso._mass_unc = parse_uncertainty(iso_mass)
iso._mass, iso._mass_unc = cast(tuple[float, float], parse_uncertainty(iso_mass))
#iso._abundance, iso._abundance_unc = parse_uncertainty(p)
iso._abundance, iso._abundance_unc = 0, 0

Expand All @@ -139,7 +140,7 @@ def init(table: PeriodicTable, reload: bool=False) -> None:
#from uncertainties import ufloat as U
#if delta > 0.01:
# print(f"{el.number}-{el.symbol} mass changed by {delta:.2f}% to {U(v,dv):fS} from {U(el._mass,el._mass_unc):fS}")
el._mass, el._mass_unc = parse_uncertainty(valstr)
el._mass, el._mass_unc = cast(tuple[float, float], parse_uncertainty(valstr))

#Li_ratio = table.Li[7]._abundance/table.Li[6]._abundance

Expand Down Expand Up @@ -176,25 +177,25 @@ def init(table: PeriodicTable, reload: bool=False) -> None:
#print(line)
parts = line.strip().split()
#print(parts)
value[int(parts[0])] = parse_uncertainty(parts[1])
value[int(parts[0])] = cast(tuple[float, float], parse_uncertainty(parts[1]))

#new_Li_ratio = table.Li[7]._abundance/table.Li[6]._abundance
#print(f"Li6:Li7 ratio changed from {Li_ratio:.1f} to {new_Li_ratio:.1f}")


def print_natural_mass(table: PeriodicTable|None=None) -> None:
from uncertainties import ufloat as U
from uncertainties import ufloat as U # type: ignore[import-untyped]
table = default_table(table)
for el in table:
iso_mass = [
U(iso.abundance, iso._abundance_unc)/100*U(iso.mass, iso._mass_unc)
U(iso.abundance, iso._abundance_unc)/100*U(iso.mass, iso._mass_unc) # type: ignore[operator]
for iso in el if iso.abundance>0]
if iso_mass:
el_mass = U(el.mass, el._mass_unc)
iso_sum = sum(iso_mass)
delta = el_mass - iso_sum
delta = el_mass - iso_sum # type: ignore[operator]
# python 3.6 and above only
if abs(delta.n) > 1e-3 or delta.s/iso_sum.n > 0.01:
if abs(delta.n) > 1e-3 or delta.s/iso_sum.n > 0.01: # type: ignore[operator,union-attr]
print(f"{el.number}-{el}: {el_mass:fS}, sum: {iso_sum:fS}, Δ={delta:fS}")
#print(f"{el.number}-{el}: {delta:fS}")
#print(f"{el.number}-{el}: {el_mass:fS}, sum: {iso_sum:fS}, Δ=")
Expand Down
13 changes: 7 additions & 6 deletions periodictable/mass_2001.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@
and High-Energy Physics, Amsterdam, The Netherlands.
"""

from typing import cast
from .core import Element, Isotope, PeriodicTable
from .mass import mass, abundance
from .util import parse_uncertainty
Expand All @@ -64,10 +65,10 @@ def init(table: PeriodicTable, reload: bool=False) -> None:
if 'mass' in table.properties and not reload:
return
table.properties.append('mass')
Element.mass = property(mass, doc=mass.__doc__)
Element.mass = property(mass, doc=mass.__doc__) # type: ignore
Element.mass_units = "u"
Isotope.mass = property(mass, doc=mass.__doc__)
Isotope.abundance = property(abundance, doc=abundance.__doc__)
Isotope.mass = property(mass, doc=mass.__doc__) # type: ignore
Isotope.abundance = property(abundance, doc=abundance.__doc__) # type: ignore
Isotope.abundance_units = "%"

for line in massdata.split('\n'):
Expand All @@ -77,9 +78,9 @@ def init(table: PeriodicTable, reload: bool=False) -> None:
assert el.symbol == sym, \
"Symbol %s does not match %s"%(sym, el.symbol)
iso = el.add_isotope(int(a))
el._mass, el._mass_unc = parse_uncertainty(avg)
iso._mass, iso._mass_unc = parse_uncertainty(m)
iso._abundance,iso._abundance_unc = parse_uncertainty(p) if p else (0,0)
el._mass, el._mass_unc = cast(tuple[float, float], parse_uncertainty(avg))
iso._mass, iso._mass_unc = cast(tuple[float, float], parse_uncertainty(m))
iso._abundance, iso._abundance_unc = cast(tuple[float, float], parse_uncertainty(p)) if p else (0, 0)

# # A single neutron is an isotope of element 0
# from .constants import neutron_mass
Expand Down
Loading