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

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

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

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

Напоминаю, что весь исходный код - здесь, на моем 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 <https://www.gnu.org/licenses/>.
"""
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 <https://www.gnu.org/licenses/>.
"""
# нам нужен будет импорт калькулятора молекулярной массы, в репозитории здесь все есть
# но т.к. это туториал, то я надеюсь, что вы сами импортируйте или вставите код
# (дабы не запутать вас)


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 <https://www.gnu.org/licenses/>.
"""
import re
from collections import Counter
# здесь нам нужен импорт или вставка кода химического элемента


def repl(m):
    return m[1] * int(m[2] if m[2] else 1)


def is_balanced(text, brackets="〈〉()[]{}"):
    opening, closing = brackets[::2], brackets[1::2]
    stack = []
    for character in text:
        if character in opening:
            stack.append(opening.index(character))
        elif character in closing:
            if stack and stack[-1] == closing.index(character):
                stack.pop()  
            else:
                return False
    return (not stack)

  
def parse_molecule(formula: str) -> dict:
    """Парсинг молекулы"""
    if not is_balanced(formula):
        raise ValueError(f"Brackets in {formula} is not balanced")
        sys.exit()
    
    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):
    """Получаем массу элемента по его химическому значку"""
    try:
        return MendeleevTable.get_element_by_shortname(element).relative_atomic_mass
    except:
        raise ValueError(f"Element {element} does not exists. Try other!")
        sys.exit()

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 <https://www.gnu.org/licenses/>.
"""
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.

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

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

Комментарии (8)


  1. smarkelov
    24.11.2023 16:15

    Я честно не понял зачем нужен класс для формул, а главное проверяется ли как-то что формула может быть с ошибкой??? Например если набор букв не будет отвечать химическому элементу.


    1. DrArgentum Автор
      24.11.2023 16:15

      Формула с ошибкой? Поясните пожалуйста, что именно вы имеет виду.


      1. smarkelov
        24.11.2023 16:15

        Например: Tm4R5


        1. DrArgentum Автор
          24.11.2023 16:15

          При вычислении молекулярной массы произойдет raise ошибки:

          ValueError: Element R does not exists. Try other!

          Но вот если при вычислении массовой доли, то будет непредвиденная ошибка.

          Сейчас исправлю код


          1. smarkelov
            24.11.2023 16:15

            И еще если перепутать порядок открывающей и закрывающей скобочки, то вроде это все должно уйти в бесконечность. То есть как-то так [ ( ] ).


            1. DrArgentum Автор
              24.11.2023 16:15

              ValueError: Braces in H[O2(Zn])CO3 is not balanced
              

              Теперь при вводе спутанных скобок выдается ошибка. Спасибо, в статье все исправлено, на гитхабе тоже


  1. Goron_Dekar
    24.11.2023 16:15
    +1

    Следующих шагов, два, чтобы выйти за пределы тривиальных калькуляторов:

    1) поддержка тривиальных частей формул. Причём разных для разных задач.

    У органиков это Ph- Me- iPr-

    У биохимиков это однобуквенные, трёхбуквенные и прочие названия аминокислот и нуклеотидов.

    2) изотопная модификация. Иногда водород весит не 1, а 2. Особенно, если ты работаешь с ЯМР.


    1. DrArgentum Автор
      24.11.2023 16:15

      Я изучаю данные моменты в теории, постепенно пытаюсь использовать их на практике