require 'matrix' module Eulim 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