Skip to content

Commit

Permalink
Add support for non-integer coefficients
Browse files Browse the repository at this point in the history
  • Loading branch information
zlatanvasovic committed Aug 9, 2020
1 parent 80a47d8 commit 9565fad
Show file tree
Hide file tree
Showing 8 changed files with 177 additions and 87 deletions.
56 changes: 42 additions & 14 deletions docs/src/examples.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,17 @@ similar to `r` prefix for regex in Julia.

```julia-repl
julia> equation = ce"Fe + Cl2 = FeCl3"
ce"Fe + Cl2 = FeCl3"
Fe + Cl2 = FeCl3
julia> balance(equation)
ce"2 Fe + 3 Cl2 = 2 FeCl3"
2 Fe + 3 Cl2 = 2 FeCl3
```

Parsing the input is insensitive to whitespace, so you don't have to be pedantic if you don't want to.

```julia-repl
julia> balance(ce"KMnO4+ HCl = KCl+MnCl2 +H2O + Cl2")
ce"2 KMnO4 + 16 HCl = 2 KCl + 2 MnCl2 + 8 H2O + 5 Cl2"
2 KMnO4 + 16 HCl = 2 KCl + 2 MnCl2 + 8 H2O + 5 Cl2
```

Parentheses (`()`), compounds written with `*` and electrical charges are all supported.
Expand All @@ -26,10 +27,10 @@ Charge is also supposed to be in any of those forms.

```julia-repl
julia> balance(ce"K4Fe(CN)6 + H2SO4 + H2O = K2SO4 + FeSO4 + (NH4)2SO4 + CO")
ce"K4FeC6N6 + 6 H2SO4 + 6 H2O = 2 K2SO4 + FeSO4 + 3 N2H8SO4 + 6 CO"
K4FeC6N6 + 6 H2SO4 + 6 H2O = 2 K2SO4 + FeSO4 + 3 N2H8SO4 + 6 CO
julia> balance(ce"Cr2O7{-2} + H{+} + {-} = Cr{+3} + H2O")
ce"Cr2O7{-2} + 14 H{+} + 6 e = 2 Cr{+3} + 7 H2O"
Cr2O7{-2} + 14 H{+} + 6 e = 2 Cr{+3} + 7 H2O
julia> balance(ce"CuSO4*5H2O = CuSO4 + H2O")
CuSO9H10 = CuSO4 + 5 H2O
Expand All @@ -38,17 +39,17 @@ CuSO9H10 = CuSO4 + 5 H2O
Even the hardest exercises are in the reach:
```julia-repl
julia> balance(ce"K4Fe(CN)6 + KMnO4 + H2SO4 = KHSO4 + Fe2(SO4)3 + MnSO4 + HNO3 + CO2 + H2O")
ce"10 K4FeC6N6 + 122 KMnO4 + 299 H2SO4 = 162 KHSO4 + 5 Fe2S3O12 + 122 MnSO4 + 60 HNO3 + 60 CO2 + 188 H2O"
10 K4FeC6N6 + 122 KMnO4 + 299 H2SO4 = 162 KHSO4 + 5 Fe2S3O12 + 122 MnSO4 + 60 HNO3 + 60 CO2 + 188 H2O
```

Writing equations with a different equal sign is also possible
(see [`ChemEquation(::AbstractString)`](@ref) for reference):
```julia-repl
julia> ce"N2+O2⇌2NO"
ce"N2 + O2 = 2 NO"
N2 + O2 = 2 NO
julia> ce"
ce"H2 + O2 = H2O"
julia> ce"H2 + O2 → H2O"
H2 + O2 = H2O
```

Are two chemical equations identical? Let's find out:
Expand All @@ -67,10 +68,10 @@ The syntax is similar, just with `cc` prefix (**c**hemical **c**ompound) instead

```julia-repl
julia> cc"CuSO4*5H2O"
cc"CuSO9H10"
CuSO9H10
julia> cc"H3O{+1}"
cc"H3O{+}
H3O{+}
```

As you could notice, input string is transformed so that every atom appears only once.
Expand All @@ -86,7 +87,7 @@ All unicode characters that are letters (such as α and β) or symbols (such as
That allows some exotic examples:
```julia-repl
julia> ce"Σ{+1} + Θ{-1} = Θ2 + Σ2"
ce"Σ{+} + Θ{-} = Θ2 + Σ2"
Σ{+} + Θ{-} = Θ2 + Σ2
```

This works because compounds are parsed by elements, where an element begins with an uppercase unicode letter and
Expand All @@ -102,10 +103,37 @@ Examples of those are ⎔ (`\hexagon`), ⬡ (`varhexagon`), ⬢ (`\varhexagonbla
Unicode input allows writing some equations very nicely:
```julia-repl
julia> ce"⏣H + Cl2 = ⏣Cl + HCl"
ce"⏣H + Cl2 = ⏣Cl + HCl"
⏣H + Cl2 = ⏣Cl + HCl
julia> ce"C + α = O + γ" # a reaction from triple-α process
ce"C + α = O + γ"
C + α = O + γ
```

## Non-integer coefficients

Sometimes coefficients in a chemical equation are written as fractions or decimals.

To initialize such equation, you need to specify the appropriate Julia type for the coefficients.
`Rational` or `Rational{Int}` is appropriate for exact fractions, while `Float64` is appropriate for decimals.
```julia-repl
julia> ChemEquation{Rational}("1//2 H2 + 1//2 Cl2 → HCl")
1//2 H2 = H
julia> ChemEquation{Float64}("0.5 H2 + 0.5 Cl2 = HCl")
0.5 H2 + 0.5 Cl2 = HCl
```
Previous two examples are equivalent (test it with `==`!), thanks to the way that numbers are stored in Julia.

You can also initialize the equation normally:
```julia-repl
julia> eq = ce"H2 + Cl2 → HCl"
H2 + Cl2 = HCl
```

and then choose to balance it with rational fractions as coefficients:
```julia-repl
julia> balance(eq, fractions=true)
1//2 H2 + 1//2 Cl2 = HCl
```

## Advanced usage
Expand Down
4 changes: 2 additions & 2 deletions docs/src/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,13 +13,13 @@ should be as simple as this:
julia> using ChemEquations
julia> equation = ce"CH4 + O2 = CO2 + H2O"
ce"CH4 + O2 = CO2 + H2O"
CH4 + O2 = CO2 + H2O
```

and balancing it should be even easier:
```julia-repl
julia> balance(equation)
ce"CH4 + 2 O2 = CO2 + 2 H2O"
CH4 + 2 O2 = CO2 + 2 H2O
```

## Installation
Expand Down
4 changes: 2 additions & 2 deletions src/ChemEquations.jl
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ Write and balance chemical equations elegantly and efficiently.
"""
module ChemEquations

using LinearAlgebraX: I, nullspacex
using LinearAlgebraX: I, nullspacex, IntegerX
using DocStringExtensions
@template (CONSTANTS, MACROS) =
"""
Expand All @@ -27,7 +27,7 @@ using DocStringExtensions
export Compound, ChemEquation,
@ce_str, @cc_str, ==, string, show,
compounds, elements, hascharge,
equationmatrix, balance
equationmatrix, balancematrix, balance

include("compound.jl")
include("chemequation.jl")
Expand Down
63 changes: 48 additions & 15 deletions src/balance.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,20 +32,36 @@ function equationmatrix(equation::ChemEquation{T}) where T
return mat
end

"Wrapper function for [`balancematrix`](@ref)."
function _balancematrix(equation)
-nullspacex(equationmatrix(equation))
end

"""
Balances an equation matrix using the *nullspace method*.
Returns an array in which each column represents a solution.
# References
- [Thorne (2009)](https://arxiv.org/ftp/arxiv/papers/1110/1110.4321.pdf)
"""
function balance(mat::AbstractMatrix)
return nullspacex(mat)
balancematrix(equation::ChemEquation) = _balancematrix(equation)

"""
Same as [`balancematrix(::ChemEquation)`](@ref), but with special format options.
By default, the solutions of integer matrices are displayed as integers.
If `fractions` is true, they will be displayed as rational fractions instead.
"""
function balancematrix(equation::ChemEquation{T}; fractions=false) where T<:IntegerX
mat = _balancematrix(equation)
if !fractions
mat ./= gcd(mat)
T.(mat)
end
return mat
end

"""
Balances the coefficients of a chemical equation.
The minimal integer solution is always displayed.
If the equation cannot be balanced, an error is thrown.
Expand All @@ -55,22 +71,40 @@ If the equation cannot be balanced, an error is thrown.
# Examples
```jldoctest
julia> balance(ce"Fe + Cl2 = FeCl3")
ce"2 Fe + 3 Cl2 = 2 FeCl3"
2 Fe + 3 Cl2 = 2 FeCl3
julia> balance(ChemEquation{Rational}("H2 + Cl2 = HCl"))
1//2 H2 + 1//2 Cl2 = HCl
"""
balance(equation::ChemEquation) = _balance(equation)

"""
Balances the coefficients of a chemical equation with integer coefficients.
The minimal integer solution is displayed by default.
If `fractions` is true, they solution will be displayed as rational fractions instead.
# Examples
```jldoctest
julia> balance(ce"Fe + Cl2 = FeCl3", fractions=true)
Fe + 3//2 Cl2 = FeCl3
julia> balance(ce"Cr2O7{-2} + H{+} + {-} = Cr{+3} + H2O")
ce"Cr2O7{-2} + 14 H{+} + 6 e = 2 Cr{+3} + 7 H2O"
Cr2O7{-2} + 14 H{+} + 6 e = 2 Cr{+3} + 7 H2O
```
"""
function balance(equation::ChemEquation; fractions=false)
eq = deepcopy(equation)
mat = balance(equationmatrix(eq))
num = size(mat)[2]
balance(equation::ChemEquation{<:IntegerX}; fractions=false) = _balance(equation, fractions)

"Wrapper function for [`balance`](@ref)."
function _balance(equation::ChemEquation, fractions=false)
if fractions
eq = ChemEquation{Rational}(equation.tuples)
mat = balancematrix(equation, fractions=true)
else
eq = ChemEquation(equation.tuples)
mat = balancematrix(equation)
end
num = size(mat)[2]
if num == 1
if !fractions
mat ./= -gcd(mat)
mat = Int.(mat)
end
for (i, k) enumerate(mat)
eq.tuples[i] = (eq.tuples[i][1], k)
end
Expand All @@ -79,6 +113,5 @@ function balance(equation::ChemEquation; fractions=false)
else
error("Chemical equation $equation cannot be balanced")
end

return eq
end
end
58 changes: 34 additions & 24 deletions src/chemequation.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
"Type stored in `ChemEquation.tuples`."
const CompoundTuple{T} = Tuple{Compound{T}, T}
const CompoundTuple{T} = Tuple{Compound, T}

fwd_arrows = ['>', '', '', '', '', '', '', '', '', '', '', '', '']
bwd_arrows = ['<', '', '', '', '', '', '', '', '', '', '', '', '']
Expand All @@ -15,13 +15,25 @@ const EQUALCHARS = vcat(fwd_arrows, bwd_arrows, double_arrows, pure_rate_arrows,
"Regex to split a chemical equation into compounds."
const PLUSREGEX = r"(?<!{)\+(?!})" # '+' not after '{' and not before '}'

"""
Stores chemical equation's compounds and their coefficients in a structured way.
"""
struct ChemEquation{T<:Number}
"Stores chemical equation's compounds and their coefficients in a structured way."
struct ChemEquation{T<:Real}
tuples::Vector{CompoundTuple{T}}
end
ChemEquation(tuples::Vector{CompoundTuple{T}}) where T = ChemEquation{T}(tuples)

"""
Constructs a chemical equation of specified type from `str`.
Follows the same rules as [`ChemEquation(::AbstractString)`](@ref).
# Examples
```jldoctest
julia> ChemEquation{Rational}("1//2 H2 → H")
1//2 H2 = H
julia> ChemEquation{Float64}("0.5 H2 + 0.5 Cl2 = HCl")
0.5 H2 + 0.5 Cl2 = HCl
```
"""
ChemEquation{T}(str::AbstractString) where T<:Real = ChemEquation(_compoundtuples(str, T))

"""
Constructs a chemical equation from the given string.
Expand All @@ -33,20 +45,19 @@ while `+` separates the equation into compounds.
# Examples
```jldoctest
julia> ChemEquation("N2+O2⇌2NO")
ce"N2 + O2 = 2 NO"
N2 + O2 = 2 NO
julia> ChemEquation("CH3COOH + Na → H2 + CH3COONa")
ce"C2H4O2 + Na = H2 + C2H3O2Na"
C2H4O2 + Na = H2 + C2H3O2Na
julia> ChemEquation("⏣H + Cl2 = ⏣Cl + HCl")
ce"⏣H + Cl2 = ⏣Cl + HCl"
⏣H + Cl2 = ⏣Cl + HCl
```
"""
ChemEquation{T}(str::AbstractString) where T<:Number = ChemEquation(compoundtuples(str, T))
ChemEquation(str::AbstractString) = ChemEquation{Int}(str)

"Extracts compound tuples from equation's string."
function compoundtuples(str::AbstractString, T::Type)
function _compoundtuples(str::AbstractString, T::Type)
strs = replace(str, ' ' => "") |>
x -> split(x, EQUALCHARS) |>
x -> split.(x, PLUSREGEX)
Expand All @@ -56,9 +67,10 @@ function compoundtuples(str::AbstractString, T::Type)

for (i, compound) enumerate(strs)
k = 1
if isdigit(compound[1])
k, compound = match(r"(^\d+)(.+)", compound).captures
k = parse(Int, k)
if isdigit(compound[1]) # begins with a digit
charindex = findfirst(r"\(?(\p{L}|\p{S})", compound)[1]
k, compound = compound[1:charindex-1], compound[charindex:end]
k = Meta.parse(k) |> eval
end
if i > splitindex
k *= -1
Expand All @@ -75,7 +87,7 @@ Constructs a chemical equation with `ce"str"` syntax, instead of `ChemEquation(s
# Examples
```jldoctest
julia> ce"H2 + O2 → H2O"
ce"H2 + O2 = H2O"
H2 + O2 = H2O
```
"""
macro ce_str(str) ChemEquation(str) end
Expand Down Expand Up @@ -112,16 +124,14 @@ function Base.string(equation::ChemEquation)
left = String[]
right = String[]
for (compound, k) equation.tuples
str = string(compound)
str = ""
if k (-1, 1)
str *= string(abs(k)) * " "
end
str *= string(compound)
if k > 0
if k 1
str = "$k " * str
end
push!(left, str)
elseif k < 0
if k -1
str = "$(-k) " * str
end
else
push!(right, str)
end
end
Expand All @@ -132,7 +142,7 @@ end

"Displays the chemical equation using [`Base.string(::Compound)`](@ref)."
function Base.show(io::IO, equation::ChemEquation)
print(io, "ce", '"', string(equation), '"')
print(io, string(equation))
end

"Returns chemical equation's compounds in a list."
Expand Down
Loading

0 comments on commit 9565fad

Please sign in to comment.