Álvaro González Sotillo

Ajuste de ecuaciones moleculares

Este proyecto es un ajustador de ecuaciones estequiométricas. Puede verse en vivo en https://alvarogonzalezsotillo.github.io/ecuacion-molecular. También puede enlazarse directamente con la ecuación ajustar.

1. Ecuaciones estequiométricas

Una ecuación estequiométrica o ecuación química muestra las moléculas iniciales de una reacción y los resultados de dicha reacción.

Por ejemplo, la reacción de combustión de hidrógeno (\(H_2\)) y oxígeno (\(O_2\)) para formar agua (\(H_{2}O\)) se representa como:

\[H_2 + O_2 = H_{2}O\]

Se puede ver que los índices moleculares no cuadran: en el lado izquierdo de la ecuación hay dos átomos de oxígeno, pero en el lado derecho solo hay uno.

Una ecuación ajustada es una en la que los coeficientes estequiométricos (cantidad de cada molécula) hace que haya el mismo número de átomos a cada lado de la ecuación. Para el caso anterior:

\[H_2 + O_2 = 2H_{2}O\]

2. Ajuste de ecuaciones

Se pueden ajustar ecuaciones estequiométricas por el método del tanteo o por el método algebraico.

El método del tanteo no es realmente un método: se van probando coeficientes hasta que la ecuación queda ajustada.

En el método algebraico se utiliza un sistema de ecuaciones lineales:

  • Las incógnitas son los coeficientes estequiométricos
  • Por cada tipo de átomo hay una ecuación
  • Los coeficientes de cada incógnita son los coeficientes moleculares del átomo en cada molécula
    • Los de la parte derecha son positivos
    • Los de la parte izquierda son negativos
  • Cada ecuación lineal se iguala a cero

Como ejemplo, se ajustará la ecuación \(H_2 + O_2= H_{2}O\). Se asigna una variable a cada molécula que será su coeficiente

\[x_0 × H2 + x_1 × O2= x_2 × H_{2}O\]

El desglose de la ecuación anterior por cada átomo da lugar a un sistema de ecuaciones \[H: 2x_0 + 0x_1 - 2x_2 = 0\] \[O: 0x_0 + 2x_1 - x_2 = 0\] Este sistema queda siempre indeterminado, pues cualquier múltiplo de los coeficientes finales será también una solución. Para definir el sistema, se añade arbitrariamente la ecuación

\[x_0=1\]

Al resolver el sistema, queda

\[x_0=1\] \[x_1=\frac{1}{2}\] \[x_2=1\]

Para conseguir coeficientes enteros, se multiplican hasta conseguir que el denominador de todos los coeficientes sea el mínimo común múltiplo de los originales. Tras ello, tenemos:

\[x_0=2\] \[x_1=1\] \[x_2=2\] Quedando la ecuación ajustada como \(2H_2 + O_2 = 2H_{2}O\)

3. Implementación

Se ha implementado la lógica en Scala, y se ha transpilado posteriormente a Javascript con Scalajs. El código fuente está disponible en un repositorio de Github, y puede probarse en vivo en https://alvarogonzalezsotillo.github.io/ecuacion-molecular.

3.1. Parseo de la ecuación

Se ha utilizado scala.util.parsing.combinator.RegexParsers para validar la ecuación introducida.

Se necesitan varias case class para representar internamente una ecuación:

  • Una EcuacionMolecular tiene dos LadoEcuacion.
  • Un LadoEcuacion tiene un número variable de Molecula.
  • Una Molecula puede ir precedida de un multiplicador, y tiene varios GrupoAtomico.
  • Un GrupoAtomico puede ser:
    • Un Atomo.
    • Un GrupoAtomico seguido de un multiplicador.
    • Varios GrupoAtomico, que aparecerán entre paréntesis.
  • Un Atomo es una cadena que empieza por mayúscula, seguido de hasta dos minúsculas.
class EcuacionMolecularParser extends RegexParsers {

  def blanco = "\\s*".r

  def atomo: Parser[Atomo] = "[A-Z][a-z]?[a-z]?".r ^^ {
    case s => Atomo(s)
  }

  def numero: Parser[Int] = "[0-9]+".r ^^ {
    case n => n.toInt
  }

  def grupo : Parser[GrupoAtomico] = rep1(("(" ~> grupo <~ ")"|atomo) ~ numero.?) ~ numero.? ^^ {
    case l ~ c =>

      val grupos = l.map {
        case grupo ~ None => grupo
        case grupo ~ cantidad => GrupoAtomico(grupo.grupos,cantidad.get)
      }

      GrupoAtomico( grupos, c.getOrElse(1))
  }

  def molecula: Parser[Molecula] = blanco ~> (numero.? ~ rep1(grupo)) <~ blanco ^^ {
    case n ~ as if  as.size == 1 && as.head.cantidad == 1 =>
      // PARA EVITAR UN EXCESO DE PARENTESIS EN LA REPRESENTACION TEXTO
      Molecula( as.head.grupos, n.getOrElse(1))
    case n ~ as =>
      Molecula( as, n.getOrElse(1))
  }

  def suma : Parser[String] = blanco ~> "\\+".r <~ blanco

  def ladoDeEcuacion : Parser[LadoEcuacion] =  molecula ~ rep( suma ~> molecula) ^^ {
    case m ~ ms => LadoEcuacion(m :: ms)
  }

  def separadorLados : Parser[String] = blanco <~ ("=".r | "<-*>".r) ~> blanco

  def ecuacion : Parser[EcuacionMolecular] =  ladoDeEcuacion ~ separadorLados ~ ladoDeEcuacion  ^^ {
    case li ~ _ ~ ld => EcuacionMolecular(li, ld)
  }

}

3.2. Explicaciones del proceso

Durante el proceso de ajuste, se generan explicaciones de los pasos seguidos. Esto se consigue a partir de literales XML volcados en un Explicador. Este explicador se pasa como parámetro implícito, se importan sus métodos explica y siExplicadorActivo para poder usarse directamente.

val variablesEnteras = {
  val denominadores = variables.map(_.den)
  val mcm = Racional.mcm(denominadores)
  val ret = variables.map( r => r.num * mcm / r.den ).map( Math.abs )

  siExplicadorActivo{
    if(denominadores.exists( _ > 1 ) ){
      explica(
        <p>
          Algunos valores de variables no son enteros.
          Multiplicaremos cada fracción hasta hacer que todos los denominadores sean el
          mínimo común múltiplo de los originales.
        </p>
      )
      explica(
        <ecuaciones>
          <ecuacion>
            mcm({denominadores.mkString(",")}) = {mcm}
          </ecuacion>
        </ecuaciones>
      )

      explica( <p>Las variables ajustadas quedan:</p> )
      explicaVariables( ret )
    }
  }
  ret
}

3.3. Ajuste de la ecuación

A partir de la ecuación molecular, se construye una matriz que representa el sistema de ecuaciones lineales descrito anteriormente.

Las ecuaciones deben resolverse con números racionales para poder reajustar las soluciones no enteras. Se ha implementado una clase Racional y su correspondiente implementación de Fractional, de forma que puede usarse de forma genérica.

Las ecuaciones se combinan linealmente para conseguir despejar las incógnitas, con una variación del método de Gauss-Jordan.

val m: Array[Array[T]] = valuesCopy()

val columns = (m(0).size min m.size)

val xml = for( col <- 0 until columns ) yield{
  val fil = m.indexWhere{ fila =>
    val noEsCero = fila(col) != cero
    val anteriores = fila.take(col)
    val anterioresCero = anteriores.forall( _ == cero )
    noEsCero && anterioresCero
  }

  for( f <- 0 until m.size if f != fil && fil != -1 ){

    val factor = m(f)(col) / m(fil)(col)
    for( c <- col until m(0).size ) {
      m(f)(c) = m(f)(c) - m(fil)(c) * factor
    }
  }

  asXML(m)
}