Химия в Python: Часть 2
Прошлая моя статья набрала хороший отклик.
И сегодня я решил написать продолжение той статьи.
Итак, к сожалению, я не смогу в этой статье показать все, что хотели комментаторы в прошлом туториале, но я постраюсь выложиться, согласно возрасту.
Хочу напомнить, что в этой статье я также уделил внимание коду.
Напоминаю, что весь исходный код — здесь, на моем GitHub’е.
Рефакторинг кода
В первой статье я добавлял элементы путем копипастинга и ручного заполнения информации об элементах.
Но я решил, что лучше будет использование готово csv-файла с информацией обо всех элементах.
А также, вместо прямого обращения, я создаю класс, который при инициализации принимает в себя список элементов и позволяет искать элемент по разным значениям.
Таблица с химическими элементами здесь.
Нам надо изменить файл с химическими элементами:
#!/usr/bin/python3
# -*- coding:utf-8 -*-
""" Oxygen Library
--------------------------------------------------------------------------------
Автор: Okulus Dev (aka DrArgentum)
Лицензия: GNU GPL v3
--------------------------------------------------------------------------------
Описание: файл с химическими элементами
Copyright (C) 2023 Okulus Dev
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
import csv
from typing import Union
def round_to_nearest(num: int):
num = int(num + (0.5 if num > 0 else -0.5))
return num
class Element:
def __init__(self, atomic_number: int, name: str, symbol: str, atomic_mass: float,
neutrons: int, protons: int, electrons: int, period: int, group: int,
phase: str, radioctive: bool, natural: bool, metall: bool, nonmetall: bool,
metalloid: bool, element_type: str):
self.atomic_number = int(atomic_number)
self.name = name
self.short_name = symbol
self.relative_atomic_mass = float(atomic_mass)
self.neutrons = int(neutrons)
self.protons = int(protons)
self.electrons = int(electrons)
self.period = int(period)
self.group = group
self.phase = phase
if natural == '':
self.natural = False
else:
self.natural = True
if radioctive == '':
self.radioctive = False
else:
self.radioctive = True
if metall == '':
self.metall = False
else:
self.metall = True
if nonmetall == '':
self.nonmetall = False
else:
self.nonmetall = True
if metalloid == '':
self.metalloid = False
else:
self.metalloid = True
if element_type == '':
self.element_type = 'Unknown'
else:
self.element_type = element_type
AVOGADRO_NUMBER = 6.02214076e23
ELEMENTS = []
# путь до csv файла
with open('oxygen/chemistry/data/PeriodicTable.csv', newline='') as File:
reader = csv.reader(File)
c = 0
for row in reader:
if c == 0:
c += 1
continue
# принимаем элементы вплоть до типа элемента
# todo: добавить другие поля
ELEMENTS.append(Element(row[0], row[1], row[2], row[3], row[4], row[5],
row[6], row[7], row[8], row[9], row[10],
row[11], row[12], row[13], row[14], row[15]))
class MendeleevTable:
def __init__(self, elements: list) -> None:
self.elements = elements
def get_element_by_shortname(self, shortname: str) -> Union[Element, None]:
for element in self.elements:
if element.short_name == shortname:
return element
return None
def get_element_by_name(self, name: str) -> Union[Element, None]:
for element in self.elements:
if element.name == name:
return element
return None
def get_element_by_number(self, num: int) -> Union[Element, None]:
for element in self.elements:
if element.atomic_number == num:
return element
return None
MendeleevTable = MendeleevTable(ELEMENTS)
Следующее, что нам понадобится — это класс химической формулы. Если пользователь ввел уже существующую химическую формулу, например сахарозу или воду, то мы выводим информацию об этом
"""
Copyright (C) 2023 Okulus Dev
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
# нам нужен будет импорт калькулятора молекулярной массы, в репозитории здесь все есть
# но т.к. это туториал, то я надеюсь, что вы сами импортируйте или вставите код
# (дабы не запутать вас)
class ChemicalFormula:
"""Класс химической формулы"""
def __init__(self, elements: dict, formula: str,
name: str, molecular_mass: float=None):
self.elements = elements
self.formula = formula
if molecular_mass is not None:
self.molecular_mass = molecular_mass
else:
self.molucular_mass = calculate_relative_molecular_mass(formula, False)
self.name = name
# Химические формулы
CHEMICAL_FORMULAS = {
'H2O': ChemicalFormula({('H', 2), ('O', 1)}, "H2O", 'Вода', None),
'C12H22O11': ChemicalFormula({('C', 12), ('H', 22), ('O', 11)},
"C12H22O11", 'Сахароза (сахар)', None)
}
def read_formula(formula: str):
"""Читаем формулу и выводим ее, если существует таковая"""
if formula in CHEMICAL_FORMULAS:
print(f'Формула {formula} это - {CHEMICAL_FORMULAS[formula].name}')
elements_in_formula = []
for el in CHEMICAL_FORMULAS[formula].elements:
elements_in_formula.append(f"{el[1]} {MendeleevTable.get_element_by_shortname(el[0]).name}")
elements_in_formula_str = ", ".join(elements_in_formula)
print(f'{CHEMICAL_FORMULAS[formula].name} состоит из {elements_in_formula_str}')
Дальше нам нужен основной код.
#!venv/bin/python3
""" Oxygen Library
--------------------------------------------------------------------------------
Автор: Okulus Dev (aka DrArgentum)
Лицензия: GNU GPL v3
--------------------------------------------------------------------------------
Описание: Базовые функции для использования химии в ваших проектах
Перечень:
1. Парсинг элементов из формулы
2. Вычисление молекулярной массы
3. Вычисление массовой доли
Copyright (C) 2023 Okulus Dev
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
import re
from collections import Counter
# здесь нам нужен импорт или вставка кода химического элемента
def repl(m):
return m[1] * int(m[2] if m[2] else 1)
def parse_molecule(formula: str) -> dict:
"""Парсинг молекулы"""
while '(' in formula:
formula = re.sub(r'\((\w*)\)(\d*)', repl, formula)
while '[' in formula:
formula = re.sub(r'\[(\w*)\](\d*)', repl, formula)
formula = re.sub(r'([A-Z][a-z]?)(\d*)', repl, formula)
formula_dict = Counter(re.findall('[A-Z][a-z]*', formula))
return formula_dict
def get_element_mass(element: str):
"""Получаем массу элемента по его химическому значку"""
return MendeleevTable.get_element_by_shortname(element).relative_atomic_mass
def calculate_mass_fraction_of_element(formula: str, element: str):
"""Вычисляем массовую долю элемента в формуле"""
formula = parse_molecule(formula)
mass_fraction = 0
mass = 0
for i in formula.items():
mass += get_element_mass(i[0]) * i[1]
for i in formula.items():
if MendeleevTable.get_element_by_shortname(i[0]).short_name == element:
mass_fraction = (get_element_mass(i[0]) * i[1] / mass) * 100
break
return mass_fraction
def calculate_relative_molecular_mass(formula: str, print_info: bool=False) -> dict:
"""Вычисляем относительную массовую долю формулы"""
result = parse_molecule(formula)
mass = 0
neutrons = 0
electrons = 0
protons = 0
for i in result.items():
try:
if print_info:
print(f'{i[1]} {MendeleevTable.get_element_by_shortname([i[0]][0]).name} = \
{MendeleevTable.get_element_by_shortname([i[0]][0]).relative_atomic_mass * i[1]}')
print(f'Кол-во протонов в {MendeleevTable.get_element_by_shortname([i[0]][0]).short_name} \
({MendeleevTable.get_element_by_shortname([i[0]][0]).name}): {MendeleevTable.get_element_by_shortname([i[0]][0]).protons}')
print(f'Кол-во электронов в {MendeleevTable.get_element_by_shortname([i[0]][0]).short_name} \
({MendeleevTable.get_element_by_shortname([i[0]][0]).name}): {MendeleevTable.get_element_by_shortname([i[0]][0]).electrons}')
print(f'Кол-во нейтронов в {MendeleevTable.get_element_by_shortname([i[0]][0]).short_name} \
({MendeleevTable.get_element_by_shortname([i[0]][0]).name}): {MendeleevTable.get_element_by_shortname([i[0]][0]).neutrons}')
neutrons += MendeleevTable.get_element_by_shortname([i[0]][0]).neutrons * i[1]
electrons += MendeleevTable.get_element_by_shortname([i[0]][0]).electrons * i[1]
protons += MendeleevTable.get_element_by_shortname([i[0]][0]).protons * i[1]
mass += get_element_mass(i[0]) * i[1]
except Exception as e:
print(e)
raise ValueError(f'Element {i[0]} does not exists. Try other!')
return {
'mass': mass,
'electrons': electrons,
'neutrons': neutrons,
'protons': protons
}
И потом мы все это подключаем к запускаемому файлу:
#!/usr/bin/python3
# -*- coding:utf-8 -*-
"""
--------------------------------------------------------------------------------
Автор: Okulus Dev (aka DrArgentum)
Лицензия: GNU GPL v3
Название: Основной файл
Файл: oxygen.py
--------------------------------------------------------------------------------
Описание: Главный файл, содержащий импорты всех библиотек и функций
Copyright (C) 2023 Okulus Dev
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
import argparse
import textwrap
# from oxygen.chemistry.base import calculate_relative_molecular_mass, \
# calculate_mass_fraction_of_element
# from oxygen.chemistry.formulas import read_formula
# сверху я привел примеры нужных функций, вставьте их код сюда или импортируйте их сами
def get_molecular_mass_from_formule(formula):
print(f'Расчет формулы {formula}:\n')
mass = calculate_relative_molecular_mass(formula, True)
if mass is not None:
print(f'Относительная молекулярная масса формулы {formula} = ~{mass["mass"]}')
print(f'Количество протонов в формуле {formula} = ~{mass["protons"]}')
print(f'Количество электронов в формуле {formula} = ~{mass["electrons"]}')
print(f'Количество нейтронов в формуле {formula} = ~{mass["neutrons"]}')
else:
print(f'Ошибка парсинга формулы {formula}')
def main():
parser = argparse.ArgumentParser(prog='Oxygen Library', allow_abbrev=True,
description='Oxygen',
formatter_class=argparse.RawDescriptionHelpFormatter,
epilog=textwrap.dedent('''
Примеры использования:
# Включаем режим химии
oxygen.py -c
# Вычисление относительной молекулярной массы формулы
oxygen.py -c -rmm <ФОРМУЛА>
# Вычисление массовой доли элемента в формуле
oxygen.py -c -mf <ФОРМУЛА> -mfe <ЭЛЕМЕНТ ИЗ ФОРМУЛЫ>
# Вычисление относительной молекулярной массы формулы с определением формулы сложного вещества
oxygen.py -cr -rmm <ФОРМУЛА>
# Вычисление массовой доли элемента в формуле с определением формулы сложного вещества
oxygen.py -cr -mf <ФОРМУЛА> -mfe <ЭЛЕМЕНТ ИЗ ФОРМУЛЫ>
Copyright Okulus Dev (C) 2023
'''))
parser.add_argument('-c', '--chemistry-mode', help='включить мод химии',
action='store_true')
parser.add_argument('-r', '--read-formula', help='включить чтение формулы',
action='store_true', default=False)
parser.add_argument('-rmm', '--relative-molecular-mass',
help='рассчет молекулярной массы формулы')
parser.add_argument('-mf', '--mass-fraction', metavar='ФОРМУЛА',
help='рассчет массовой доли в формуле')
parser.add_argument('-mfe', '--mf-element', metavar='ФОРМУЛА',
help='элемент для рассчета массовой доли')
args = parser.parse_args()
if args.chemistry_mode:
if args.relative_molecular_mass:
get_molecular_mass_from_formule(args.relative_molecular_mass)
if args.read_formula:
read_formula(args.relative_molecular_mass)
elif args.mass_fraction:
if args.mf_element:
res = calculate_mass_fraction_of_element(args.mass_fraction,
args.mf_element)
if args.read_formula:
read_formula(args.mass_fraction)
print(f"Массовая доля {args.mf_element} в \
{args.mass_fraction}: {res}%")
else:
print('К сожалению, вы не указали нужный элемент.')
if args.read_formula:
read_formula(args.mass_fraction)
if __name__ == "__main__":
main()
Здесь мы остановимся. Я также добавил парсер аргументов командной строки argparse.
Примеры использования:
# Включаем режим химии
oxygen.py -c
# Вычисление относительной молекулярной массы формулы
oxygen.py -c -rmm <ФОРМУЛА>
# Вычисление массовой доли элемента в формуле
oxygen.py -c -mf <ФОРМУЛА> -mfe <ЭЛЕМЕНТ ИЗ ФОРМУЛЫ>
# Вычисление относительной молекулярной массы формулы с определением формулы сложного вещества
oxygen.py -cr -rmm <ФОРМУЛА>
# Вычисление массовой доли элемента в формуле с определением формулы сложного вещества
oxygen.py -cr -mf <ФОРМУЛА> -mfe <ЭЛЕМЕНТ ИЗ ФОРМУЛЫ>
Argparse позволяет комбинировать флаги, и вместо -c -r можно писать -cr.
Также я сократил названия функций и добавил их названия.
В следующей статье мы займемся вычислительной химией глубоко. С вами был доктор Аргентум, всем пока!