7
$\begingroup$

Background

I written a chemical equation balancing program.

elem=Alternatives@@Reverse@SortBy[StringLength]@Array[ElementData[#,"Symbol"]&,112];
chem=StringCases[#,e:elem~~n:DigitCharacter...:>{e,n}]/.""->"1"&;
group=List@@@Normal[GroupBy[#,First->Last,Total@ToExpression[#]&]]&;
name2mat=group/@chem[ToString/@#]&;

ChemicalSolver[input_List]:=Block[{all,elemts,null},
  all=name2mat[input];
  elemts=Union[First@Transpose[Flatten[all,1]]];
  null=NullSpace@Transpose[(elemts/.Rule@@@#&/@all)/._String->0];
  Thread[input->Transpose@null]
];

Give a chemical equation:

$$C_2H_5OH+O_2==H_2O+CO_2$$

The first part name2mat change the chemicals to lists, then ChemicalSolver calculate the nullspace and find the solution.

enter image description here

So one of the solutions is :

$$C_2H_5OH+3O_2==3H_2O+2CO_2$$


Problems

But there are some flaws, firstly it can't recognize the brackets, like Al2(CO3)3.

Secondly can't recognize the non-existent element, (These symbols not in ElementData), a symbol like Abcd also can be a kind of element. Some times they'll use abbreviations in organic chemistry.

※ Thirdly can't recognize the ion reactions like "(CO3)2-", this maybe hard, it's {{"C", 1}, {"O", 3}, {"e", -2}}, this is not necessary. ChemicalSolver still works on this case I think.

※ Small problem, ChemicalSolver can't recognize reactant. Output of NullSpace's positive and negative looks random.


Goal

A function expr2mat overcome these shortcomings. Or just expr2rule then rule2mat.

Input:

double="C2H5OH+O2=H2O+CO2"
brackets="Al2(CO3)3+H2SO4=Al2(SO4)3+H2O+CO2"
ions="MnO4^{-} +SO3^{2-}+6H^{1+}=Mn^{2+}+SO4^{2-}+H2O"

Output:

{{{"C",2},{"H",6},{"O",1}},{{"H",2},{"O",1}},{{"O",2}},{{"C",1},{"O",2}}} 

or whole matrix, and I think need 2 list to mark the matrix.

\begin{array}{c|cccccc} {}&MnO_4^-&SO_3^{2-}&H^+&Mn^{2+}&SO_4^{2-}&H_2O\\ \hline Mn& 1&0&0&1&0&0 \\ O & 4&3&0&0&4&1 \\ S & 0&1&0&0&1&0 \\ H & 0&0&1&0&0&2 \\ e & – 1&- 2&1&2&- 2&0 \end{array}


Edit

  1. Add more input test.

  2. C2H5OH with double H also a possible situation.

  3. You can use your own marking if you think that can make things easier. For example use "Ca^{2+}"represent ions.

  4. ※ Consider coordination complex if possible, like "Fe[N(Si(CH3)3)2]3"or written as "Fe(N(Si(CH3)3)2)3".

$\endgroup$
  • $\begingroup$ BTW, this is something WolframAlpha already does. So an alternative is to use WolframAlpha and related functions. Here is an example of chemical equation balancing: WolframAlpha["C2H5OH+O2==H2O+CO2", {{"BalancedEquationPod:ChemicalReactionData", 1}, "Content"}]. $\endgroup$ – Anton Antonov Dec 9 '17 at 15:43
  • $\begingroup$ Interesting, looks it depends on its powerful database, when I try "Ck2H5OH + O2 == H2O + CkO2", WolframAlpha really confused. $\endgroup$ – GalAster Dec 10 '17 at 6:08
  • $\begingroup$ "looks it depends on its powerful database" -- Yes, of course. "Ck" is not a known chemical element. So WolframAlpha is not confused. $\endgroup$ – Anton Antonov Dec 10 '17 at 14:48
3
$\begingroup$

So the best way to do this would be to look up what others have done in other languages and reimplement that in Mathematica.

That's not what I did.

Instead I wrote an iterative formula splitter that works solely on the basis of three tokens:

  1. Whitespace
  2. Parens
  3. Capital Letters

Here's the code:

chemSplit1[s_] :=
  StringSplit[s,
   t : "(" | ")" :> t
   ];
chemSplit2[s_] :=
  StringSplit[s,
   c : LetterCharacter?(Not@*LowerCaseQ) :>
    "!~" <> c
   ];
chemSplit3[s_] :=
  StringSplit[#,
     n : NumberString :> ToExpression@n
     ] & /@ s;
chemSplit[s_] :=

  chemSplit3@*chemSplit2@*chemSplit1 /@ StringSplit[s];
chemNormalize[s_] :=
  s /.
   {
    cap_String?(StringStartsQ["!~"]) :>
     "Start" -> StringTrim[cap, "!~"],
    n_?NumericQ :> "End" -> n,
    "(" -> ("BlockStart" -> None),
    ")" -> ("BlockEnd" -> None)
    };
chemRegroup1[l_] :=
  Block[{splitVal = 0},
   SplitBy[l,
    Replace[
     {
      ("Start" -> _) :>
       (++splitVal),
      ("End" -> _) :>
       (splitVal++),
      ("BlockStart" -> _) :>
       (++splitVal),
      ("BlockEnd" -> _) :>
       (++splitVal),
      _ :>
       splitVal
      }
     ]
    ]
   ];
chemRegroup2[l_] :=
  Block[{splitVal = 0},
   SplitBy[l,
    Replace[
     {
      {("BlockStart" -> _), ___} :>
       (++splitVal),
      {("BlockEnd" -> _), ___} :>
       (splitVal++),
      _ :> splitVal
      }
     ]
    ]
   ];
chemRegroup[l_] :=
  chemRegroup2@
   chemRegroup1[Flatten@l];
chemReform[l_] :=
  Map[
   Replace[
     Replace[
      DeleteCases[# /. (_ -> a_) :> a, None, 2],
      {
       {s__String, n_Integer} :>
        StringJoin[s] -> n,
       {s__String} :>
        StringJoin[s] -> 1,
       {} -> Nothing
       },
      1
      ],
     {
      {a__, {n_Integer}} :>
       {a} -> n,
      {a__} :>
       {a} -> 1
      }
     ] &,
   l
   ];
$chemNameMap =
  {
   "OAc" -> "Oac"
   };
chemPreNormalize[s_] :=
  StringReplace[s, $chemNameMap];
chemPostClean[s_] :=
  With[{revMap = Reverse /@ $chemNameMap},
   s /. {
     o_String :>
      StringReplace[o, revMap]
     }
   ];
chemFormulaInterpret[s_] :=
 chemReform[
     chemRegroup@
      chemNormalize[#]
     ] & /@ chemSplit[chemPreNormalize[s]] // chemPostClean

It's mad inefficient and could probably be done with a sophisticated enough StringCases, but it does get the splits right, even for things that don't exists, like "Ch".

chemFormulaInterpret["(Ch4NH3)3(Ch6)2"]

{{{"Ch" -> 4, "N" -> 1, "H" -> 3} -> 3, {"Ch" -> 6} -> 2}}

This would fail on things like "OAc", but I added a white-list with $chemNameMap:

chemFormulaInterpret["Cr(OAc6)"]

{{{"Cr" -> 1} -> 1, {"OAc" -> 6} -> 1}}

Note that I used nested Rule instead of nested List as the final form to assuage some ambiguities.

Getting this into your format should be easy enough.

One thing I haven't handled, but which this could pretty quickly be extended to handle properly, is charges. Note, though, that things like:

chemFormulaInterpret["SO4-2"]

{{{"S" -> 1, "O" -> 4, "" -> -2} -> 1}}

actually work by complete accident.

So if you're willing to invert the way you write your charges (and willing to assume "" is the tag for "Charge") you should be fine.

$\endgroup$
  • $\begingroup$ My fault, ions may use special markings like "^{-}"or something, then it can be Identify from equation easier. How to solve this StringSplit["Ca^{2+}+ HCO3^{1-}+OH^{1-}=CaCO3+H2O", {"+", "="}] $\endgroup$ – GalAster Dec 9 '17 at 6:05
  • $\begingroup$ And something go wrong with "Fe(N(Si(CH3)3)2)3", I think chemNormalize miss the BlockStart under double brackets. $\endgroup$ – GalAster Dec 10 '17 at 3:52
  • $\begingroup$ @GalAster seems reasonable. I'll try to patch it up sometime when I've got the energy to do so. $\endgroup$ – b3m2a1 Dec 10 '17 at 5:22

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.