Ir para o conteúdo principal

Top 3 bibliotecas Python para química

·2576 palavras·13 minutos·
Ciência Python Química Molmass Mendeleev Chempy Matplotlib
Autor
Helena Benevenuto Soares
Química e Analista de Dados
Tabela de conteúdos

Top 3 bibliotecas Python para química
#

Estudantes e profissionais da área química precisam realizar diversos cálculos no dia a dia. Tais cálculos muitas vezes são complexos devido à grande quantidade de dados e de aproximações. Ferramentas computacionais facilitam a resolução dessas atividades.

Nesse artigo, vamos explorar 3 bibliotecas químicas disponíveis para a linguagem Python, mostrando como elas são úteis na nossa rotina.

Molmass
#

Começamos nosso Top 3 com o Molmass, um pacote Python que permite calcular a massa molar de uma forma bem simples. Ao invés de calcular tediosamente as massas molares de compostos somando as dos seus elementos constituintes, basta passar sua fórmula, como veremos.

A instalação pode ser feita via pip install molmass. E, para importar:

from molmass import Formula

Vamos começar com um exemplo simples: cerveja! Mais especificamente, o etanol presente nas cervejas.

etanol
Representação da molécula de etanol

Podemos calcular a massa molar do etanol facilmente com o Molmass. Para isso, passamos uma string com a fórmula do etanol para a classe Formula e solicitamos o atributo mass, a unidade utilizada é g/mol, como informado no comentário:

formula = Formula('C2H5OH')
formula.mass  # g/mol
46.068531

Podemos observar que o pacote utiliza as massas molares sem arredondamentos, o que é excelente, pois o uso de aproximações leva à propagação de erros.

Ah, e se quiser continuar o papo de cerveja, veja como ela já deu origem à uma biblioteca Python nesse artigo. Mas só depois de terminar este artigo aqui :-)

O Molmass aceita diferentes entradas para um mesmo composto. Vamos passar outra string com a abreviação clássica do radical etil na orgânica, Et, para obter a massa molar do etanol:

formula = Formula('EtOH')
formula.mass
46.068531

Observe que as fórmulas diferenciam maiúsculas de minúsculas.

Para obter informações da composição elementar do composto, podemos utilizar o método composition:

formula.composition()
(('C', 2, 24.02148, 0.5214292593788155),
 ('H', 6, 6.047646, 0.13127499116479316),
 ('O', 1, 15.999405, 0.34729574945639136))

Vemos que o método composition fornece colunas com os atributos element, number, relative_mass e fraction, respectivamente.

Mendeleev
#

Outro pacote Python muito útil no nosso dia a dia é o Mendeleev. Ele permite acessar várias propriedades de elementos, íons e isótopos.

O pacote pode ser instalado via pip install mendeleev.

Tabela periódica
#

Uma das caracteristicas mais legais do Mendeleev é a possibilidade de importar uma tabela periódica com apenas uma linha de código. Para isso precisamos ter a dependência vis, que pode ser instalada via pip install mendeleev[vis].

Uma maneira de visualizar a tabela periódica é usando a função periodic_table do módulo mendeleev.vis:

from mendeleev.vis import periodic_table

Para observar a visualização padrão da tabela periódica vamos chamar a função importada:

periodic_table()

periodic_table

Propriedades de elementos
#

Agora de um ponto de vista mais técnico. Para acessar os dados de um elemento basta importar por meio de seus símbolos. Por exemplo, considere o cromo:

from mendeleev import Cr

Para extrair alguma informação podemos solicitar um atributo, por exemplo, o boiling_point onde vamos obter a temperatura de ebulição em Kelvin, como informado no comentário:

Cr.boiling_point  # K
2945.0

Uma forma alternativa para acessar os dados é por meio do element, método que retorna uma única instância ou uma tupla dessas instâncias, dependendo dos argumentos:

from mendeleev import element

O método element aceita o nome do elemento em inglês, o símbolo atômico ou o número atômico. Vejamos um exemplo em que passamos o número atômico do bromo para o método element e solicitamos o atributo atomic_radius onde vamos obter o raio atômico em picometros, como informado no comentário:

bromo = element(35)
bromo.atomic_radius  # pm
115.0

O método element também aceita lista ou tupla. Observe esse exemplo em que passamos uma lista para o método element e solicitamos o atributo econf onde vamos obter a configuração eletronica dos elementos:

hidrogenio, zinco, bromo = element(['Hydrogen', 'Zn', 35]) 
hidrogenio.econf, zinco.econf, bromo.econf
('1s', '[Ar] 3d10 4s2', '[Ar] 3d10 4s2 4p5')

Com o Mendeleev também conseguimos obter a configuração eletrônica do íon a partir do elemento. Tomemos como exemplo o íon Fe3+. Primeiramente, passamos o nome do elemento em inglês para o método element e solicitamos o atributo ec onde vamos obter a configuração eletrônica do átomo de ferro:

ferro = element('Iron')
ferro.ec
<ElectronicConfiguration(conf="1s2 2s2 2p6 3s2 3p6 3d6 4s2")>

Para obter a configuração eletrônica do íon Fe3+ vamos utilizar o método ionize a fim de remover os 2 elétrons da última camada eletrônica e não do subnível mais energético e 1 elétron, agora sim, do subnível mais energético, retornando a seguinte configuração eletrônica:

ferro.ec.ionize(3)
<ElectronicConfiguration(conf="1s2 2s2 2p6 3s2 3p6 3d5")>

Existem alguns atributos como o isotopes que retorna uma lista de objetos com as propriedades de cada isótopo atomic_number, mass_number, mass e abundance, respectivamente:

enxofre = element('S')
for iso in enxofre.isotopes:
    print(iso)
   16    32   31.97207 0.944
   16    33   32.97146 0.008
   16    34   33.96787 0.048
   16    36   35.96708 0.000

O ionic_radii retorna uma lista de objetos com os atributos charge, coordination, crystal_radius em pm, e ionic_radius em pm:

ouro = element('Au')
for ir in ouro.ionic_radii:
    print(ir)
charge=   1, coordination=VI   , crystal_radius=151.000, ionic_radius=137.000
charge=   3, coordination=IVSQ , crystal_radius=82.000, ionic_radius=68.000
charge=   3, coordination=VI   , crystal_radius=99.000, ionic_radius=85.000
charge=   5, coordination=VI   , crystal_radius=71.000, ionic_radius=57.000

O ionenergies retorna um dicionário com energias de ionização:

oxigenio = element('O')
oxigenio.ionenergies
{1: 13.618054,
 2: 35.12111,
 3: 54.93554,
 4: 77.4135,
 5: 113.8989,
 6: 138.1189,
 7: 739.32679,
 8: 871.40985}

Outro atributo é o oxistates que retorna uma lista dos estados de oxidação mais comuns para um determinado elemento:

manganes = element('Mn')
manganes.oxistates
[7, 6, 4, 3, 2]

O método element possui vários outros atributos e/ou métodos que nos permitem acessar diversas propriedades dos elementos. Vamos observar todos os disponíveis no objeto manganes que criamos:

manganes = element('Mn')
[x for x in dir(manganes) if not x.startswith('_')]
['abundance_crust',
 'abundance_sea',
 'annotation',
 'atomic_number',
 'atomic_radius',
 'atomic_radius_rahm',
 'atomic_volume',
 'atomic_weight',
 'atomic_weight_uncertainty',
 'block',
 'boiling_point',
 'c6',
 'c6_gb',
 'cas',
 'covalent_radius',
 'covalent_radius_bragg',
 'covalent_radius_cordero',
 'covalent_radius_pyykko',
 'covalent_radius_pyykko_double',
 'covalent_radius_pyykko_triple',
 'cpk_color',
 'density',
 'description',
 'dipole_polarizability',
 'dipole_polarizability_unc',
 'discoverers',
 'discovery_location',
 'discovery_year',
 'ec',
 'econf',
 'electron_affinity',
 'electronegativity',
 'electronegativity_allen',
 'electronegativity_allred_rochow',
 'electronegativity_cottrell_sutton',
 'electronegativity_ghosh',
 'electronegativity_gordy',
 'electronegativity_li_xue',
 'electronegativity_martynov_batsanov',
 'electronegativity_mulliken',
 'electronegativity_nagle',
 'electronegativity_pauling',
 'electronegativity_sanderson',
 'electronegativity_scales',
 'electrons',
 'electrophilicity',
 'en_allen',
 'en_ghosh',
 'en_pauling',
 'evaporation_heat',
 'fusion_heat',
 'gas_basicity',
 'geochemical_class',
 'glawe_number',
 'goldschmidt_class',
 'group',
 'group_id',
 'hardness',
 'heat_of_formation',
 'init_on_load',
 'ionenergies',
 'ionic_radii',
 'is_monoisotopic',
 'is_radioactive',
 'isotopes',
 'jmol_color',
 'lattice_constant',
 'lattice_structure',
 'mass',
 'mass_number',
 'mass_str',
 'melting_point',
 'mendeleev_number',
 'metadata',
 'metallic_radius',
 'metallic_radius_c12',
 'molcas_gv_color',
 'name',
 'name_origin',
 'neutrons',
 'nvalence',
 'oxides',
 'oxistates',
 'period',
 'pettifor_number',
 'proton_affinity',
 'protons',
 'registry',
 'sconst',
 'screening_constants',
 'series',
 'softness',
 'sources',
 'specific_heat',
 'symbol',
 'thermal_conductivity',
 'uses',
 'vdw_radius',
 'vdw_radius_alvarez',
 'vdw_radius_batsanov',
 'vdw_radius_bondi',
 'vdw_radius_dreiding',
 'vdw_radius_mm3',
 'vdw_radius_rt',
 'vdw_radius_truhlar',
 'vdw_radius_uff',
 'zeff']

Propriedades de íons
#

O mendeleev possui a classe Ion para trabalhar com íons ao invés de elementos. Os íons podem ser criados a partir de elementos e informações de carga:

from mendeleev.ion import Ion

ferro_3 = Ion("Fe", 3)

Vamos acessar algumas propriedades dos íons:

ferro_3.electrons
23
ferro_3.name
'Iron 3+ ion'
ferro_3.unicode_ion_symbol()
'Fe³⁺'

ChemPy
#

ChemPy é um pacote Python que permite resolver problemas de química analítica, físico-química e inorgânica.

O pacote pode ser instalado via pip install chempy.

Balanceamento de equações
#

Com o ChemPy podemos balancear os coeficientes estequiométricos de uma reação. Para importar, simplesmente:

from chempy import balance_stoichiometry

Vamos imaginar um exemplo em que precisamos balancear a reação de combustão do acetileno, gás combustível empregado em maçaricos.

maçarico
Acetileno é o combustível de alguns maçaricos

$$ C_2H_2 + O_2 \rightarrow CO_2 + H_2O $$

Para isso, passamos os reagentes e os produtos para a função balance_stoichiometry:

balance_stoichiometry({'C2H2', 'O2'}, {'CO', 'H2O'})
(OrderedDict([('C2H2', 2), ('O2', 3)]), OrderedDict([('CO', 4), ('H2O', 2)]))

Obtivemos dois dicionários, um para os reagentes e outro para os produtos, com os coeficientes estequiométricos que balanceiam a equação.

Vamos agora considerar outro exemplo para mostrar que podemos obter a equação balanceada diretamente. Balancearemos a seguinte reação, que é utilizada em boosters de foguetes espaciais para auxiliar no lançamento dos mesmos:

$$ NH_4ClO_4 + Al \rightarrow Al_2O_3 + HCl + H_2O + N_2 $$
reac, prod = balance_stoichiometry({'NH4ClO4', 'Al'}, {'Al2O3', 'HCl', 'H2O', 'N2'})

reac
OrderedDict([('Al', 10), ('NH4ClO4', 6)])
prod
OrderedDict([('Al2O3', 5), ('H2O', 9), ('HCl', 6), ('N2', 3)])

Dessa maneira retorna um dicionário ordenado, solicitando a função dict criamos um dicionário padrão:

dict(reac)
{'Al': 10, 'NH4ClO4': 6}
dict(prod)
{'Al2O3': 5, 'H2O': 9, 'HCl': 6, 'N2': 3}

Podemos construir a equação balanceada usando a classe Reaction:

from chempy import Reaction

reaction = Reaction(reac, prod)
reaction
10 Al + 6 NH4ClO4 -> 5 Al2O3 + 9 H2O + 6 HCl + 3 N2

Sistemas redox
#

Dentre os experimentos mais bonitos e interessantes de química temos as reações que mudam de cor. Esses experimentos de mudança de cor podem ocorrer, por exemplo, como resultado de reações de redução e oxidação.

KMnO4
Reação de permanganato de potássio com peróxido de hidrogênio

No exemplo do GIF, a mudança de cor ocorre devido íons roxos de permanganato, MnO4-, receberem elétrons para formar íons incolores de manganês(II), Mn+2. Além disso, a formação de gás ocorre porque o peróxido de hidrogênio, H2O2, e o oxigênio do MnO4- são convertidos em H2O e O2.

Caso você precise balancear essa reação redox, também conseguimos com o ChemPy. Vamos obter a equação global balanceada das seguintes semi-reações:

$$ \begin{align*} MnO_4^- + 8H^+ + 5e^- &\rightleftharpoons Mn^{+2} + 4H_2O \\ H_2O_2 &\rightleftharpoons O_2 + 2H^+ + 2e^- \end{align*} $$

Para isso passamos os reagentes e os produtos das respectivas semi-reações para a classe Equilibrium:

from chempy import Equilibrium

sr_red = Equilibrium({'MnO4-': 1, 'H+': 8, 'e-': 5}, {'Mn+2': 1, 'H2O': 4})
sr_ox = Equilibrium({'H2O2': 1}, {'O2': 1, 'H+':2, 'e-':2})

Agora precisamos eliminar os elétrons e multiplicar cada reação pelos seus respectivos coeficientes para obter a equação global balanceada:

coeff = Equilibrium.eliminate([sr_red, sr_ox], 'e-')
coeff
[2, 5]
redox = sr_red * coeff[0] + sr_ox * coeff[1]
redox
6 H+ + 5 H2O2 + 2 MnO4- -> 8 H2O + 2 Mn+2 + 5 O2

Cinética química
#

Além de balanceamento, o Chempy é muito útil no estudo de cinética química.

A equação de Arrhenius fornece a dependência da constante de velocidade com a temperatura como:

$$ k = A \exp \left( -\frac{E_a}{RT} \right) $$

Tomando o logaritmo natural da equação de Arrhenius e reorganizando a equação, temos a forma de uma equação linear:

$$ \ln k = - \frac{E_a}{R}\frac{1}{T} + \ln A $$

O ChemPy possibilita obter a energia de ativação e o parâmetro A utilizando a função fit_arrhenius_equation. Vamos começar importando a função:

from chempy.kinetics.arrhenius import fit_arrhenius_equation

Agora vamos passar um conjunto de valores de temperatura em Kelvin, de constante de velocidade em L/(mol.s) e erros da constante de velocidade. Para isso, vamos importar a biblioteca NumPy a fim de criar os arrays e para usar posteriormente as funções logaritmo natural (log) e exponencial (exp):

import numpy as np

temperatures = np.array((1125, 1053, 1001, 838))    # K
k_values = np.array((11.59, 1.67, 0.380, 0.0011))   # L / (mol.s)
k_errors = np.array((1E-2, 1E-2, 1E-3, 1E-5))

Passando as variáveis temperatures, k_values e k_errors para a função fit_arrhenius_equation vamos obter os valores da energia de ativação e do parâmetro A:

fit = fit_arrhenius_equation(temperatures, k_values, kerr=k_errors)
fit
(array([5.13001705e+12, 2.51435472e+05]),
 array([[inf, inf],
        [inf, inf]]))

Internamente, fit_arrhenius_equation utiliza o método curve_fit do SciPy. O retorno são dois arrays. O primeiro com os valores dos parâmetros; o segundo com as covariâncias quando forem possíveis de serem calculadas.

Para criar as variáveis do parâmetro A e da energia de ativação vamos acessar os elementos do primeiro array, para isso é bem simples. O primeiro array é ([5.13001705e+12, 2.51435472e+05]) cujos valores podem ser acessados como segue:

A = fit[0][0]

activation_energy = fit[0][1]

print(f'A = {A:.3e} L/(mol.s) \nEa = {activation_energy:.3e} J/mol')
A = 5.130e+12 L/(mol.s) 
Ea = 2.514e+05 J/mol

Também podemos obter a energia de ativação e o parâmetro A com a representação gráfica de \(\ln k\) vs \(\frac{1}{T}\).

import matplotlib.pyplot as plt
from scipy.stats import linregress

# plot config
params = {
    'lines.linewidth': 2.0,
    'axes.facecolor': 'white',
    'figure.facecolor': 'white',
    'axes.labelsize': 14,
    'axes.titlesize': 16,    
    'xtick.labelsize': 12,
    'ytick.labelsize': 12,
    'figure.autolayout': True,
    'figure.titlesize': 18,    
    'figure.figsize': (12, 6),
    'legend.shadow': False,    
    'legend.fontsize': 14,
}

plt.rcParams.update(params)

# data points
fig, (ax1, ax2) = plt.subplots(nrows=1, ncols=2)
ax1.errorbar(temperatures, k_values, yerr=k_errors, marker='o', linestyle='')
ax2.scatter(1/temperatures, np.log(k_values), label='Data')

# regression
slope, intercept, r_value, p_value, std_err = linregress(1/temperatures, np.log(k_values))
regression = slope * 1/temperatures + intercept
ax2.plot(1/temperatures, regression, 
         color='red', alpha=0.5,
         label=r'$\ln k={:.0f}\frac{{1}}{{T}}+{:.2f}$'.format(slope,intercept))
ax2.legend()

# visuals
for ax in (ax1, ax2):
    ax.ticklabel_format(style='sci', axis='x', scilimits=(0,0))
    ax.xaxis.major.formatter._useMathText = True
    ax.figure.canvas.draw()
    order_magnitude = ax.xaxis.get_offset_text().get_text().replace('\\times', '')    
    ax.xaxis.offsetText.set_visible(False)
    if ax == ax1:
        ax.set_xlabel(r'$T$ / ' + order_magnitude + ' $K$', )
        ax.set_ylabel(r'$k / \frac{\ell}{mol \cdot s}$')
        ax.set_title('Forma exponencial', color='dimgray')
    else:
        ax.set_xlabel(r'$\frac{1}{T}$ / ' + order_magnitude + ' $K^{-1}$', )
        ax.set_ylabel(r'$\ln k$')
        ax.set_title('Forma linearizada', color='dimgray')

plt.suptitle('Gráficos para a equação de Arrhenius', 
             color='dimgray')
plt.show()

png

Caso queira ver como o gráfico foi gerado com a biblioteca Matplotlib veja esse repositório.

Sistemas em equilíbrio
#

O ChemPy também permite efetuar cálculos de pH. Imagine um exemplo em que precisamos descobrir o pH de uma solução 0,015 mol/L de acetato.

$$ \begin{align*} H_3CCOOH &\rightleftharpoons H^+ + H_3CCOO^- && K_a = 10^{-4.756} \\ H_2O &\rightleftharpoons H^+ + OH^- && K_w = 10^{-14} \\ \end{align*} $$

Vamos começar criando um sistema com as equações de equilíbrio e suas constantes:

from chempy.equilibria import EqSystem

equation = 'H3CCOOH = H+ + H3CCOO-; 10**-4.756'
water_autoionization = 'H2O = H+ + OH-; 10**-14/55.5'

system = EqSystem.from_string('\n'.join((equation, water_autoionization)))

Para o ChemPy conseguir resolver o sistema e fornecer as concentrações de todas as espécies em solução, precisamos passar as concentrações das espécies que são conhecidas. A concentração de acetato é de 0,015 mol/L e a concentração molecular da água é de 55,5 mol/L:

from collections import defaultdict

condition = defaultdict(float, {'H2O': 55.5, 'H3CCOO-': 0.015})

concentrations, _, _ = system.root(condition)
result = dict(zip(system.substances, concentrations))
result
{'H+': 3.4177702795991742e-09,
 'H2O': 55.499997074115974,
 'H3CCOO-': 0.014997077533741783,
 'H3CCOOH': 2.922466258210992e-06,
 'OH-': 2.9258840281057227e-06}

Passando a concentração de H+ para a seguinte função, retorna o pH:

def pH(hydronium_concentration):
    return -np.log10(hydronium_concentration)

print(f"{pH(result['H+']):.2f}")
8.47

Também podemos obter o diagrama de espécies para o ácido acético. Para isso, precisamos das frações molares do acetato e do ácido acético. Vamos criar uma função que recebe todas as espécies do sistema e retira as espécies que não serão utilizadas, retornando um dicionário das frações molares das espécies de interesse, acetato e ácido acético:

def mole_fractions(substances):
    for key in ('H+', 'OH-', 'H2O'):
        substances.pop(key, None)
    frac = dict()
    total = sum(substances.values())
    for key, value in substances.items():
        frac[key] = value / total
    return frac

Agora vamos criar um dicionário para armazenar as frações molares das espécies de interesse e criar o range de pH. O fractions foi inicialmente criado como uma lista vazia para posteriormente armazenar os valores das frações molares para cada valor de pH.

species = [item for item in system.substance_labels() if item not in ('H+', 'OH-', 'H2O')]
fractions = defaultdict(list, {k: [] for k in species})

pH_range = np.arange(0, 14.1, 0.1)

for value in pH_range:
    condition = defaultdict(float, {'H2O': 55.5, 'H+': 10**-value})
    concentrations, _, _ = system.root(condition)
    result = dict(zip(system.substances, concentrations))
    mf = mole_fractions(result)
    for key, value in zip(fractions.keys(), mf.values()):
        fractions[key].append(value)

Podemos obter o gráfico de diagrama das espécies utilizando a biblioteca Matplotlib. Para isso, vamos criar uma figura, passar os eixos e identificar o gráfico:

fig, ax = plt.subplots()

for compound in fractions:
    ax.plot(pH_range, fractions[compound], 
            label=f'${system.substances[compound].latex_name}$')

ax.set_xlabel('pH')
ax.set_ylabel(r'$\alpha$')
ax.grid(b=True, axis='both', which='major', linestyle='--', linewidth=1.5)
ax.minorticks_on()
ax.grid(b=True, axis='both', which='minor', linestyle=':', linewidth=1.0)
ax.set_axisbelow(True)

ax.legend()
ax.set_title('Diagrama de espécies para o ácido acético', color='dimgray')

plt.show()

png

Conclusão
#

Nesse artigo, vimos algumas funcionalidades das bibliotecas Molmass, Mendeleev e ChemPy e o quanto essas ferramentas são úteis na resolução de atividades da rotina de estudantes e profissionais da área química. Vimos que podemos agilizar cálculos simples, como de massa molar, até mais complexos como de concentração de espécies e de cinética.

Este artigo é uma colaboração minha, Helena Benevenuto, com o Ciência Programada. Me acompanhe no LinkedIn para mais conteúdos de química.

Compartilhe este artigo em suas redes e siga o projeto Ciência Programada para sempre estar atualizado. Até a próxima.

Relacionados

ENEM com Python - a questão da inversão da sacarose (2020)
·2837 palavras·14 minutos
ENEM Tecnológico Python Molmass Química Matplotlib Rdkit
Será que é possível estudar para o vestibular e ainda aprender Python? Sim, claro! Neste artigo, analisaremos uma questão da prova de 2020. É uma questão de química, mas que pode ser resolvida mais com raciocínio do que com química em si.
Estudando nossa atmosfera com Python
·3616 palavras·17 minutos
Ciência Python Pint Chempy Web Scraping Matplotlib Pandas Scipy Regex
Incolor, de constituintes invisíveis e inodoro (espera-se!). Por vezes nos esquecemos que estamos rodeados de gases que constituem nossa atmosfera. Nesse artigo, vamos ver como podemos facilmente, com a linguagem Python, conseguir uma listagem dos principais constituintes do ar com web scrapping. E, também, fazer algumas contas como, por exemplo, de densidade e de massa molar média e, claro, apresentar tudo em gráficos espetaculares.
ENEM - A questão do tijolo assassino
·3171 palavras·15 minutos
ENEM Tecnológico Python Pint Chempy Web Scraping Matplotlib Pandas Scipy Regex
Será que é possível estudar para o vestibular e ainda aprender Python? Sim, claro! E é esse um dos objetivos da série ENEM Tecnológico aqui do site. Nessa série, utilizaremos ferramentas computacionais para analisar e expandir questões do ENEM. No artigo de hoje, analisaremos uma questão da prova de 2019. E, de quebra, aprenderemos a fazer gráficos 3D e 4D(?!) com Matplotlib. Tudo devido a um tijolo assassino.