from typing import List, Union
import pandas as pd
from sqlalchemy.dialects import sqlite
from mendeleev import element
from mendeleev import __version__ as version
from .db import get_engine, get_session
from .models import Element, IonizationEnergy
def get_zeff(an, method: str = "slater") -> float:
"""
A helper function to calculate the effective nuclear charge.
Args:
method: str
Method to use, one of "slater" or "clementi", default="slater"
Returns:
zeff: float
Effective nuclear charge
"""
e = element(an)
return e.zeff(method=method)
[docs]
def fetch_table(table: str, **kwargs) -> pd.DataFrame:
"""
Return a table from the database as :py:class:`pandas.DataFrame`
Args:
table: Name of the table from the database
kwargs: A dictionary of keyword arguments to pass to the :py:func:`pandas.read_qsl`
Returns:
df (pandas.DataFrame): Pandas DataFrame with the contents of the table
Example:
>>> from mendeleev.fetch import fetch_table
>>> df = fetch_table('elements')
>>> type(df)
pandas.core.frame.DataFrame
"""
tables = {
"elements",
"groups",
"ionicradii",
"ionizationenergies",
"isotopedecaymodes",
"isotopes",
"oxidationstates",
"phasetransitions",
"screeningconstants",
"series",
}
if table not in tables:
raise ValueError(
f"Table '{table}' not found, available tables are: {', '.join(sorted(tables))}"
)
engine = get_engine()
return pd.read_sql(table, engine, **kwargs)
[docs]
def fetch_electronegativities(scales: List[str] = None) -> pd.DataFrame:
"""
Fetch electronegativity scales for all elements as :py:class:`pandas.DataFrame`
Args:
scales: list of scale names, defaults to all available scales
Returns:
df (pandas.DataFrame): Pandas DataFrame with the contents of the table
"""
session = get_session()
engine = get_engine()
query = session.query(Element.atomic_number).order_by("atomic_number")
df = pd.read_sql_query(query.statement.compile(dialect=sqlite.dialect()), engine)
scales = [
"allen",
"allred-rochow",
"cottrell-sutton",
"ghosh",
"gordy",
"li-xue",
"martynov-batsanov",
"mulliken",
"nagle",
"pauling",
"sanderson",
]
for scale in scales:
scale_name = "-".join(s.capitalize() for s in scale.split("-"))
df.loc[:, scale_name] = [
element(int(row.atomic_number)).electronegativity(scale=scale)
for _, row in df.iterrows()
]
return df.set_index("atomic_number")
[docs]
def fetch_ionization_energies(degree: Union[List[int], int] = 1) -> pd.DataFrame:
"""
Fetch a :py:class:`pandas.DataFrame` with ionization energies for all elements
indexed by atomic number.
Args:
degree: Degree of ionization, either as int or a list of ints. If a list is
passed then the output will contain ionization energies corresponding
to particalr degrees in columns.
Returns:
df (pandas.DataFrame): ionization energies, indexed by atomic number
"""
# validate degree
if isinstance(degree, (list, tuple, set)):
if not all(isinstance(d, int) for d in degree) or any(d <= 0 for d in degree):
raise ValueError("degree should be a list of positive ints")
elif isinstance(degree, int):
if degree <= 0:
raise ValueError("degree should be positive")
degree = [degree]
else:
raise ValueError(
f"degree should be either a positive int or a collection of positive ints, got: {degree}"
)
session = get_session()
engine = get_engine()
query = session.query(Element.atomic_number).order_by("atomic_number")
df = pd.read_sql_query(query.statement.compile(dialect=sqlite.dialect()), engine)
for d in degree:
query = session.query(IonizationEnergy).filter(IonizationEnergy.degree == d)
energies = pd.read_sql_query(
query.statement.compile(dialect=sqlite.dialect()), engine
)
df = pd.merge(
df,
energies.loc[:, ["atomic_number", "energy"]].rename(
columns={"energy": "IE{0:d}".format(d)}
),
on="atomic_number",
how="left",
)
return df.set_index("atomic_number")
def fetch_neutral_data() -> pd.DataFrame:
"""
Get extensive set of data from multiple database tables as pandas.DataFrame
"""
elements = fetch_table("elements")
series = fetch_table("series")
groups = fetch_table("groups")
elements = pd.merge(
elements,
series,
left_on="series_id",
right_on="id",
how="left",
suffixes=("", "_series"),
)
elements = pd.merge(
elements,
groups,
left_on="group_id",
right_on="group_id",
how="left",
suffixes=("", "_group"),
)
elements.rename(columns={"color": "series_colors"}, inplace=True)
for attr in ["hardness", "softness"]:
elements[attr] = [
getattr(element(row.symbol), attr)() for _, row in elements.iterrows()
]
elements["mass"] = [
element(row.symbol).mass_str() for _, row in elements.iterrows()
]
elements.loc[:, "zeff_slater"] = elements.apply(
lambda x: get_zeff(x["atomic_number"], method="slater"), axis=1
)
elements.loc[:, "zeff_clementi"] = elements.apply(
lambda x: get_zeff(x["atomic_number"], method="clementi"), axis=1
)
ens = fetch_electronegativities()
elements = pd.merge(elements, ens.reset_index(), on="atomic_number", how="left")
ies = fetch_ionization_energies(degree=1)
elements = pd.merge(elements, ies.reset_index(), on="atomic_number", how="left")
return elements
[docs]
def fetch_ionic_radii(radius: str = "ionic_radius") -> pd.DataFrame:
"""
Fetch a pandas DataFrame with ionic radii for all the elements.
Args:
radius: The radius to be returned either `ionic_radius` or `crystal_radius`
Returns:
df (pandas.DataFrame): a table with atomic numbers, symbols and ionic radii for all
coordination numbers
"""
if radius not in ["ionic_radius", "crystal_radius"]:
raise ValueError(
"radius '{radius}', not found, available radii are: 'ionic_radius', 'crystal_radius'"
)
ir = fetch_table("ionicradii")
return ir.pivot_table(
columns="coordination", values=radius, index=["atomic_number", "charge"]
)
def add_plot_columns(elements: pd.DataFrame) -> pd.DataFrame:
"""
Add columns needed for the creating the plots
Args:
elements: pd.DataFrame
"""
mask = elements["group_id"].notnull()
elements.loc[mask, "x"] = elements.loc[mask, "group_id"].astype(int)
elements.loc[:, "y"] = elements.loc[:, "period"].astype(int)
elements.loc[mask, "group_name"] = (
elements.loc[mask, "group_id"].astype(int).astype(str)
)
elements.loc[~mask, "group_name"] = "f block"
for period in [6, 7]:
mask = (elements["block"] == "f") & (elements["period"] == period)
elements.loc[mask, "x"] = (
elements.loc[mask, "atomic_number"]
- elements.loc[mask, "atomic_number"].min()
+ 3
)
elements.loc[mask, "y"] = elements.loc[mask, "period"] + 2.5
# additional columns for positioning of the text
elements.loc[:, "y_symbol"] = elements["y"] - 0.05
elements.loc[:, "y_anumber"] = elements["y"] - 0.3
elements.loc[:, "y_name"] = elements["y"] + 0.18
return elements
def get_app_data() -> None:
"write a file with the neutral data"
data = fetch_neutral_data()
data = add_plot_columns(data)
fname = "neutral_{0:s}.pkl".format(version)
data.to_pickle(fname)
print("wrote file: ", fname)