Химия в Python: Часть 2

75248d8b7986701fac3a61371471f322.jpg

Прошлая моя статья набрала хороший отклик.

И сегодня я решил написать продолжение той статьи.

Итак, к сожалению, я не смогу в этой статье показать все, что хотели комментаторы в прошлом туториале, но я постраюсь выложиться, согласно возрасту.

Хочу напомнить, что в этой статье я также уделил внимание коду.

Напоминаю, что весь исходный код — здесь, на моем 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.

Также я сократил названия функций и добавил их названия.

В следующей статье мы займемся вычислительной химией глубоко. С вами был доктор Аргентум, всем пока!

© Habrahabr.ru