lib/ilm/chemistry/reaction.rb
module Ilm
module Chemistry
# This class has functionality for reaction
# Ex: check for balanced rxn, validity of a rxn
class Reaction
attr_accessor :equation, :is_valid, :species, :participants, :balanced_eqn, :rate_equation
STATES = {
'(s)' => 'solid', '(l)' => 'liquid',
'(g)' => 'gaseous', '(aq)' => 'aqueous',
'' => ''
}.freeze
def initialize(args)
# rate_eqn should be of the form: 'r_{CaO} = k[CaO][CO2]'
@equation = args[:equation]
@species = build_species
@is_valid = valid_rxn?
participant_elements
@balanced_eqn = balance_rxn
@rate_equation = validify_rate_eqn args[:rate_equation]
end
private
def validify_rate_eqn(rate_eqn)
if rate_eqn
rate_eqn.gsub('_{', '')
.gsub('}', '')
.gsub('[', ' * c')
.gsub(']', '')
else
specie = @species[:reactants].keys.first
'r' + specie + ' = k * c' + specie
end
end
def build_species
r = {}
result = {}
r[:reactants], r[:products] = @equation.split('>>')
r.each do |type, _type_species|
result[type] = {}
r[type].split('+').each do |specie|
result[type].merge!(get_specie_info(specie.strip))
end
end
result
end
def get_specie_info(specie)
sc = get_stoichiometry specie
st = get_state specie
offset_sc = sc.zero? ? 0 : sc.to_s.length
offset_st = st.empty? ? 0 : st.length
specie_str = specie[offset_sc..(specie.length - offset_st - 1)]
{
specie_str => {
compound: Compound.new(specie_str),
stoichiometry: sc.zero? ? 1 : sc,
state: STATES[st]
}
}
end
def balanced_rxn?
bal = { reactants: {}, products: {} }
@species.each do |type, type_species|
type_species.each do |_specie, s_info|
s_info[:compound].constituents.each do |sym, c_info|
bal[type][sym] ||= 0
bal[type][sym] += c_info[:atom_count] * s_info[:stoichiometry]
end
end
end
bal[:products] == bal[:reactants]
end
def valid_rxn?
valid = {}
@species.each do |type, type_species|
valid[type] = []
type_species.each do |_specie, info|
info[:compound].constituents.each do |symbol, _count|
valid[type] << symbol
end
end
end
valid[:reactants].sort.uniq == valid[:products].sort.uniq
end
def get_stoichiometry(specie)
specie.match(/^\d*/).to_a.first.to_i
end
def get_state(specie)
specie.match(/\((s|l|g|aq)\)$/).to_s
end
def participant_elements
participants = []
@species[:reactants].keys.each do |r|
participants << @species[:reactants][r][:compound].constituents.keys
end
@participants = participants.flatten.uniq
end
def get_participant_row(parti)
row = []
@species.keys.each do |key|
i = key == :reactants ? 1 : -1
@species[key].keys.each do |specie|
if specie.include? parti
row << @species[key][specie][:compound]
.constituents[parti][:atom_count] * i
else
row << 0
end
end
end
row
end
def write_matrix
@matrix = Matrix[]
@participants.each do |parti|
@matrix = Matrix.rows(@matrix.to_a << get_participant_row(parti))
end
@matrix
end
def balanced_coeff_array
write_matrix
null_space_array = @matrix.nullspace_array
lcm = null_space_array.collect(&:denominator).reduce(1, :lcm)
null_space_array.collect { |x| (x * lcm).to_i }
end
def balance_rxn
exp = ''
i = 0
bal_coeff = balanced_coeff_array
@species.keys.each do |key|
@species[key].keys.each do |comp|
coeff = bal_coeff[i] == 1 ? ' ' : ' ' + bal_coeff[i].abs.to_s
state = STATES.key(@species[key][comp][:state])
exp = exp + coeff + comp + state + ' +'
i += 1
end
exp = key == :reactants ? exp.chomp('+') + '>>' : exp.chomp('+')
end
exp.strip
end
end
end
end