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 dosLadoEcuacion
. - Un
LadoEcuacion
tiene un número variable deMolecula
. - Una
Molecula
puede ir precedida de un multiplicador, y tiene variosGrupoAtomico
. - Un
GrupoAtomico
puede ser:- Un
Atomo
. - Un
GrupoAtomico
seguido de un multiplicador. - Varios
GrupoAtomico
, que aparecerán entre paréntesis.
- Un
- 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) }