Балансировка химических уравнений с использованием Python

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

Здесь я опишу свой теоретический подход к проблеме в Python, используя лишь некоторый код, и вы сможете понять, что происходит. Я сделаю ссылку на код на GitHub, чтобы увидеть настоящий код.

Уравновешивание химических уравнений - обычное занятие в классах средней школы и за ее пределами. Вопрос в том (и почти для любого вида деятельности) - можем ли мы автоматизировать этот процесс?

Ответ - смелое «да», и есть несколько способов подойти к этой проблеме. Мой может быть не самым быстрым, но он точен для всех химических уравнений, которые не включают многоатомное разложение. Чтобы уточнить это предостережение, многоатомный атом на одной стороне уравнения разбивается на отдельные части - этот алгоритм может обрабатывать только многоатомные ионы, которые остаются (такими же) многоатомными ионами на другой стороне уравнения.

Вот моя программа, работающая над тремя все более сложными уравнениями балансировки (правда, без многоатомного разложения!)

Обобщенный подход к уравновешиванию уравнений

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

Вместо этого мы хотим рассматривать проблему балансировки химического уравнения как систему линейных уравнений. Давайте возьмем простое химическое уравнение и попробуем это сделать.

В нашем уравнении участвуют три разных элемента. Селен (Se), хлорид (Cl) и кислород (O). Мы хотим найти четыре пробела, которые мы можем переназначить переменным x, y, z, t.

Теперь мы можем собрать вектор . Этот вектор будет иметь длину, равную количеству элементов в нашем уравнении, и для каждого члена мы будем использовать вектор, чтобы записать, какие и сколько элементов присутствовали [в члене].

Теперь у нас есть уравнение линейных уравнений. Затем мы можем переместить комбинацию из правой части уравнения в левую. Это сделает векторы справа от стрелки отрицательными (что эквивалентно отрицательным значениям переменных).

Мы можем легко превратить это в эквивалентную матричную систему уравнений в виде Ax = b.

Теперь решение для наших коэффициентов x, y, z, и t - это просто вопрос поиска оптимального вектора нулевого пространства x, который не включает нулей (у нас не может быть нулевого коэффициента в нашей задаче балансировки) или десятичных знаков (не может иметь половину моля) и является как можно меньшим. Это сложная часть проблемы кодирования.

Использование Python

Мы можем закодировать эту систему на Python, и проблема разделится на несколько отдельных частей. Я буду использовать псевдокод, чтобы объяснить свой метод, и ссылку на репозиторий GitHub внизу.

Проблему можно разделить на два основных этапа. Разбор входных данных и решение для наших коэффициентов x с использованием линейной алгебры.

Анализ ввода

Первое препятствие - это синтаксический анализ ввода. Чтобы все было удобно и напоминало сами химические уравнения, я сделал вводные данные как можно ближе к реальным. Единственное предостережение: нижние индексы стали большими числами (вы не можете вводить нижний индекс), поэтому индексы были представлены числами после элементов.

Еще одно предостережение заключается в том, что, в отличие от реальных уравнений, этап синтаксического анализа входных данных намного проще, если вы заставляете пользователей говорить «1», даже если имеется один атом.

Последнее предостережение заключается в том, что нельзя писать числа внутри многоатомного выражения. Несмотря на то, что внутри многоатомных ионов часто есть нижние индексы, обозначающие количество присутствующих в них элементов, мы допускаем только общий нижний индекс, указывающий , сколько здесь многоатомных атомов.

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

Затем я начал что-то в этом роде. Я сохраняю это расплывчатым, но надеюсь, имея небольшой опыт работы с Python, вы сможете это сделать или найти более разумный способ.

def parseInput(input):
   # split string into lhs and rhs of arrow
   rhs = input.split(' -> ')[1]
   lhs = input.split(' -> ')[0]
   # find unique elements in equation 
   uniquevals = rhs.unique()
   # make dict of each element's position on the vectors
   vecdict = dict{'firstelem':0, 'secelem':1......} 
   # find vectors for each term
   termvectors = []
   for term in lhs:
     termvector = np.zeros[uniquevals, 1]
     # split term into elements and subscripts
     for e, element in enumerate(term):
        termvector[vecdict[element]] = amtofelement
     termvectors.append(termvector)
   for term in rhs:
     termvector = np.zeros[uniquevals, 1]
     # split term into elements and subscripts
   for e, element in enumerate(term):
        termvector[vecdict[element]] = amtofelement
   termvectors.append(-1*termvector) # since these are moved over

Цель в конце этапа синтаксического анализа - получить все ваши векторы, которые мы теперь сохранили в нашем списке векторов termvectors.

Решение для x

Теперь мы можем довольно быстро собрать нашу матрицу, объединив наши векторы в termvectors в одну большую матрицу A, создав нулевой вектор того же размера, что и все другие векторы термов, и решив для нулевого пространства x.

Я использовал sympy, пакет символьной математики, для их команды с нулевым пространством, которая, как я обнаружил, работает намного лучше, чем NumPy (в данном случае), поскольку NumPy автоматически делает нулевое пространство ортонормированным, чего я не хотел.

Дело в том, что теперь у нас есть векторы нулевого пространства (векторы, которые определяют нулевое пространство матрицы, это, вероятно, не очень хорошие ответы. Они либо содержат отрицательные значения, что недопустимо в нашем контексте. Чаще всего они будут содержать нецелые числа Например, в нашем текущем примере вектор нулевого пространства (в данном случае единственный, поскольку у нас был только один зависимый столбец) выглядел так:

Итак: в подавляющем большинстве случаев начальные векторы нулевого пространства не являются ответами для наших коэффициентов.

Особенность векторов нулевого пространства в том, что они образуют основу для нулевого пространства. Это означает, что любой вектор в нулевом пространстве (который включает в себя желаемые ответы) может быть представлен как линейная комбинация базисных векторов нулевого пространства, что дает matrix.nullspace().

Итак, теперь мы ищем, на какие коэффициенты нам нужно умножить матрицы нулевого пространства, чтобы получить выходной ответ.

Самое главное - придумать определение для оптимального ответа коэффициентов. Мы знаем, что они:

  • Должен содержать только целые числа
  • Должен содержать только положительные числа
  • Не может содержать 0

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

Итак, мы хотим найти наименьший вектор, который удовлетворяет всем критериям. Мы измеряем размер вектора, используя его норму (подумайте о длине вектора в пространстве). Существует много, много разных типов норм, но мы будем использовать, вероятно, самый стандартный, норму L 2. Обычно это предустановка для большинства пакетов с какой-либо командой .norm().

So:

Мы ищем коэффициенты, которые умножают наши векторы нулевого пространства, чтобы получить вектор ответа (x), а именно:

  • Все положительное
  • Все целые числа
  • Все ненулевые
  • Имеет наименьшую норму L2 среди всех других жизнеспособных решений

Интересно то, что не удобной функции, алгоритма или даже математической модели, которая эффективно находит некоторую комбинацию векторов нулевого пространства, согласующуюся с этими правилами. Фактически, это [небольшая] активная тема исследований в области числовой линейной алгебры сегодня, как вы можете видеть, насколько это может быть удобно.

Итак, используем метод перебора. Мы выбираем список чисел от 1 до n, устанавливаем каждый из коэффициентов x на каждое из чисел из списка (таким образом, чтобы мы получали все возможные комбинации значений коэффициентов).

Эти числа будут неотрицательными целыми числами, поскольку нет причин использовать в качестве коэффициентов нецелые или отрицательные числа, поскольку они просто гарантируют результаты, которых мы не хотим.

Мы можем определить, каким должно быть это n, посмотрев на большинство химических уравнений. Большинство химических уравнений не нужно балансировать, вставляя перед ними огромные коэффициенты (ну, во всяком случае, те, с которыми вы сталкиваетесь в классе). Установите предел n на все, что имеет смысл в вашей ситуации. Для меня оптимальный диапазон для большинства задач - от 1 до 15.

Итак, попробовав все комбинации переменных от 1 до 15 *, я беру каждый из результирующих векторов (из линейных комбинаций для каждой отдельной перестановки значений коэффициентов) и помещаю их в список.

* Ради эффективности я на самом деле не беру линейную комбинацию - я просто объединяю все векторы нулевого пространства в одну матрицу N и умножаю на наши присвоенные значения для коэффициентов x1 и x2 ... Это дает нам тот же ответ .

Давайте посмотрим, как это «формализовано» в некотором псевдокоде, исходя из того, откуда мы взяли наши векторы на этапе синтаксического анализа.

def solve(termvectors)
   A = concatenate(termvectors)
   
   # using sympy
   from sympy import Matrix, nullspace
   A = Matrix(A)
   nvecs = A.nullspace()
   N = concatenate(nvecs) 
   # create range of possible coefficient vals and make combination
   range = list(range(1, n))
   allpossiblevals = range*numberOfNullspaceVectors
   
   combos = itertools.combinations(allpossiblevals)
   allvecs = []
   for combo in combos:
      currentvector = np.zeros
      
      # assign each coefficient to number in combos
      for e, var in enumerate(combo): 
         currentvector[e] = var
      allvecs.append(currentvector)     

Затем я возвращаю тот, у которого наименьшая норма L2 в качестве ответа. Мы можем сделать это с помощью цикла вспомогательных функций, который проверяет каждый вектор в списке, чтобы увидеть, содержит ли он нецелые числа, нули или отрицательные числа. Это немного сложно, поскольку они все с плавающей запятой, поэтому нам нужно специально проверять наличие чисел с плавающей запятой без цифр после запятой. К счастью, мы можем использовать (float).is_integer().

bestscore = (#norm(set to 0), #vector(set to None))
for vec in allvecs:
   if vector contains 0:
       continue 
   elif vector contains 0 < x < 1: 
       continue 
   elif vector contains < 0:
       continue 
   
   elif vec.norm() > bestscore[0]:
       continue
   elif vec.norm() < bestscore[0]:
       bestscore[0] = vec.norm
       bestscore[1] = vec

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

Вот результат этого алгоритма, теперь, когда мы знаем, как он все это делает.

Меня действительно удивило, насколько быстро он работал, несмотря на то, что требовалось много-много разных циклов for и умножений матриц. Для них я установил n равным 20, что означало, что за это время нужно было проверить около 800 различных векторов, дисквалифицировать плохие, сравнить их нормы и вернуть лучший.

Спасибо, что просмотрели это - я надеюсь, вам понравится, добавляя к этому!

Вот репозиторий GitHub. * Я не устанавливал n как переменную, но установил в коде значение 20 как безопасную величину.

Адам Дхалла - ученик средней школы из Ванкувера, Британская Колумбия. Он увлечен миром активного отдыха и в настоящее время изучает новейшие технологии в экологических целях. Посетите его сайт adamdhalla.com.

Следите за его I nstagram и его LinkedIn. Чтобы получить больше похожего контента, подпишитесь на его информационный бюллетень здесь.