From db175807103172d2f7f91e5e72f1fae2c3ebca16 Mon Sep 17 00:00:00 2001 From: Victor Shnayder Date: Tue, 9 Oct 2012 18:47:32 -0400 Subject: [PATCH] Chemcalc refactor, improvement * Move tests into a separate file * add a chemical_equations_equal function to compare equations, not expressions * rename some internal functions with a leading _ --- common/lib/capa/capa/chem/chemcalc.py | 340 ++++++-------------------- common/lib/capa/capa/chem/tests.py | 296 ++++++++++++++++++++++ 2 files changed, 377 insertions(+), 259 deletions(-) create mode 100644 common/lib/capa/capa/chem/tests.py diff --git a/common/lib/capa/capa/chem/chemcalc.py b/common/lib/capa/capa/chem/chemcalc.py index 65d1887d2e..b2198b5537 100644 --- a/common/lib/capa/capa/chem/chemcalc.py +++ b/common/lib/capa/capa/chem/chemcalc.py @@ -1,27 +1,19 @@ from __future__ import division import copy +from fractions import Fraction import logging import math import operator import re -import unittest import numpy import numbers import scipy.constants -from pyparsing import Literal, Keyword, Word, nums, StringEnd, Optional, Forward, OneOrMore -from pyparsing import ParseException +from pyparsing import (Literal, Keyword, Word, nums, StringEnd, Optional, + Forward, OneOrMore, ParseException) import nltk from nltk.tree import Tree -local_debug = None - - -def log(s, output_type=None): - if local_debug: - print s - if output_type == 'html': - f.write(s + '\n
\n') ## Defines a simple pyparsing tokenizer for chemical equations elements = ['Ac','Ag','Al','Am','Ar','As','At','Au','B','Ba','Be', @@ -42,19 +34,19 @@ tokens = reduce(lambda a, b: a ^ b, map(Literal, elements + digits + symbols + p tokenizer = OneOrMore(tokens) + StringEnd() -def orjoin(l): +def _orjoin(l): return "'" + "' | '".join(l) + "'" -## Defines an NLTK parser for tokenized equations +## Defines an NLTK parser for tokenized expressions grammar = """ S -> multimolecule | multimolecule '+' S multimolecule -> count molecule | molecule count -> number | number '/' number molecule -> unphased | unphased phase unphased -> group | paren_group_round | paren_group_square - element -> """ + orjoin(elements) + """ - digit -> """ + orjoin(digits) + """ - phase -> """ + orjoin(phases) + """ + element -> """ + _orjoin(elements) + """ + digit -> """ + _orjoin(digits) + """ + phase -> """ + _orjoin(phases) + """ number -> digit | digit number group -> suffixed | suffixed group paren_group_round -> '(' group ')' @@ -70,7 +62,7 @@ grammar = """ parser = nltk.ChartParser(nltk.parse_cfg(grammar)) -def clean_parse_tree(tree): +def _clean_parse_tree(tree): ''' The parse tree contains a lot of redundant nodes. E.g. paren_groups have groups as children, etc. This will clean up the tree. @@ -119,7 +111,7 @@ def clean_parse_tree(tree): children = [] for child in tree: - child = clean_parse_tree(child) + child = _clean_parse_tree(child) children.append(child) tree = nltk.tree.Tree(tree.node, children) @@ -127,7 +119,7 @@ def clean_parse_tree(tree): return tree -def merge_children(tree, tags): +def _merge_children(tree, tags): ''' nltk, by documentation, cannot do arbitrary length groups. Instead of: (group 1 2 3 4) @@ -157,13 +149,13 @@ def merge_children(tree, tags): # And recurse children = [] for child in tree: - children.append(merge_children(child, tags)) + children.append(_merge_children(child, tags)) #return tree return nltk.tree.Tree(tree.node, children) -def render_to_html(tree): +def _render_to_html(tree): ''' Renders a cleaned tree to HTML ''' def molecule_count(tree, children): @@ -196,29 +188,29 @@ def render_to_html(tree): if type(tree) == str: return tree else: - children = "".join(map(render_to_html, tree)) + children = "".join(map(_render_to_html, tree)) if tree.node in dispatch: return dispatch[tree.node](tree, children) else: return children.replace(' ', '') -def clean_and_render_to_html(s): +def render_to_html(s): ''' render a string to html ''' - status = render_to_html(get_finale_tree(s)) + status = _render_to_html(_get_final_tree(s)) return status -def get_finale_tree(s): +def _get_final_tree(s): ''' return final tree after merge and clean ''' tokenized = tokenizer.parseString(s) parsed = parser.parse(tokenized) - merged = merge_children(parsed, {'S','group'}) - final = clean_parse_tree(merged) + merged = _merge_children(parsed, {'S','group'}) + final = _clean_parse_tree(merged) return final -def check_equality(tuple1, tuple2): +def _check_equality(tuple1, tuple2): ''' return True if tuples of multimolecules are equal ''' list1 = list(tuple1) list2 = list(tuple2) @@ -242,18 +234,31 @@ def compare_chemical_expression(s1, s2, ignore_state=False): def divide_chemical_expression(s1, s2, ignore_state=False): - ''' Compare chemical equations for difference - in factors. Ideas: + '''Compare two chemical equations for equivalence up to a multiplicative factor: + + - If they are not the same chemicals, returns False. + - If they are the same, "divide" s1 by s2 to returns a factor x such that s1 / s2 == x as a Fraction object. + - if ignore_state is True, ignores phases when doing the comparison. + + Examples: + divide_chemical_expression("H2O", "3H2O") -> Fraction(1,3) + divide_chemical_expression("3H2O", "H2O") -> 3 # actually Fraction(3, 1), but compares == to 3. + divide_chemical_expression("2H2O(s) + 2CO2", "H2O(s)+CO2") -> 2 + divide_chemical_expression("H2O(s) + CO2", "3H2O(s)+2CO2") -> False + + Implementation sketch: - extract factors and phases to standalone lists, - compare equations without factors and phases, - divide lists of factors for each other and check for equality of every element in list, - - return result of factor division ''' + - return result of factor division + + ''' # parsed final trees treedic = {} - treedic['1'] = get_finale_tree(s1) - treedic['2'] = get_finale_tree(s2) + treedic['1'] = _get_final_tree(s1) + treedic['2'] = _get_final_tree(s2) # strip phases and factors # collect factors in list @@ -290,7 +295,7 @@ def divide_chemical_expression(s1, s2, ignore_state=False): *sorted(zip(treedic['2 cleaned_mm_list'], treedic['2 factors'], treedic['2 phases']))) # check if equations are correct without factors - if not check_equality(treedic['1 cleaned_mm_list'], treedic['2 cleaned_mm_list']): + if not _check_equality(treedic['1 cleaned_mm_list'], treedic['2 cleaned_mm_list']): return False # phases are ruled by ingore_state flag @@ -300,241 +305,58 @@ def divide_chemical_expression(s1, s2, ignore_state=False): if any(map(lambda x, y: x / y - treedic['1 factors'][0] / treedic['2 factors'][0], treedic['1 factors'], treedic['2 factors'])): - log('factors are not proportional') + # factors are not proportional return False - else: # return ratio - return int(max(treedic['1 factors'][0] / treedic['2 factors'][0], - treedic['2 factors'][0] / treedic['1 factors'][0])) + else: + # return ratio + return Fraction(treedic['1 factors'][0] / treedic['2 factors'][0]) -class Test_Compare_Equations(unittest.TestCase): +def chemical_equations_equal(eq1, eq2, ignoreFactor=True): + """ + Check whether two chemical equations are the same. If ignoreFactor is True, + then they are considered equal if they differ by a constant factor. - def test_compare_incorrect_order_of_atoms_in_molecule(self): - self.assertFalse(compare_chemical_expression("H2O + CO2", "O2C + OH2")) + arrows matter: ->, and <-> are different. - def test_compare_same_order_no_phases_no_factors_no_ions(self): - self.assertTrue(compare_chemical_expression("H2O + CO2", "CO2+H2O")) + e.g. + chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + H2 -> H2O2') -> True + chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + 2H2 -> H2O2') -> False - def test_compare_different_order_no_phases_no_factors_no_ions(self): - self.assertTrue(compare_chemical_expression("H2O + CO2", "CO2 + H2O")) + chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + H2 <-> H2O2') -> False - def test_compare_different_order_three_multimolecule(self): - self.assertTrue(compare_chemical_expression("H2O + Fe(OH)3 + CO2", "CO2 + H2O + Fe(OH)3")) + If there's a syntax error, we raise pyparsing.ParseException. + """ + # for now, we do a manual parse for the arrow. + arrows = ('<->', '->') # order matters -- need to try <-> first + def split_on_arrow(s): + """Split a string on an arrow. Returns left, arrow, right, or raises ParseException if there isn't an arrow""" + for arrow in arrows: + left, a, right = s.partition(arrow) + if a != '': + return left, a, right + raise ParseException("Could not find arrow. Legal arrows: {0}".format(arrows)) - def test_compare_same_factors(self): - self.assertTrue(compare_chemical_expression("3H2O + 2CO2", "2CO2 + 3H2O ")) + left1, arrow1, right1 = split_on_arrow(eq1) + left2, arrow2, right2 = split_on_arrow(eq2) - def test_compare_different_factors(self): - self.assertFalse(compare_chemical_expression("2H2O + 3CO2", "2CO2 + 3H2O ")) + # TODO: may want to be able to give student helpful feedback about why things didn't work. + if arrow1 != arrow2: + # arrows don't match + return False - def test_compare_correct_ions(self): - self.assertTrue(compare_chemical_expression("H^+ + OH^-", " OH^- + H^+ ")) + factor_left = divide_chemical_expression(left1, left2) + if not factor_left: + # left sides don't match + return False - def test_compare_wrong_ions(self): - self.assertFalse(compare_chemical_expression("H^+ + OH^-", " OH^- + H^- ")) + factor_right = divide_chemical_expression(right1, right2) + if not factor_right: + # right sides don't match + return False - def test_compare_parent_groups_ions(self): - self.assertTrue(compare_chemical_expression("Fe(OH)^2- + (OH)^-", " (OH)^- + Fe(OH)^2- ")) + if factor_left != factor_right: + # factors don't match (molecule counts to add up) + return False - def test_compare_correct_factors_ions_and_one(self): - self.assertTrue(compare_chemical_expression("3H^+ + 2OH^-", " 2OH^- + 3H^+ ")) - - def test_compare_wrong_factors_ions(self): - self.assertFalse(compare_chemical_expression("2H^+ + 3OH^-", " 2OH^- + 3H^+ ")) - - def test_compare_float_factors(self): - self.assertTrue(compare_chemical_expression("7/2H^+ + 3/5OH^-", " 3/5OH^- + 7/2H^+ ")) - - # Phases tests - def test_compare_phases_ignored(self): - self.assertTrue(compare_chemical_expression( - "H2O(s) + CO2", "H2O+CO2", ignore_state=True)) - - def test_compare_phases_not_ignored_explicitly(self): - self.assertFalse(compare_chemical_expression( - "H2O(s) + CO2", "H2O+CO2", ignore_state=False)) - - def test_compare_phases_not_ignored(self): # same as previous - self.assertFalse(compare_chemical_expression( - "H2O(s) + CO2", "H2O+CO2")) - - def test_compare_phases_not_ignored_explicitly(self): - self.assertTrue(compare_chemical_expression( - "H2O(s) + CO2", "H2O(s)+CO2", ignore_state=False)) - - # all in one cases - def test_complex_additivity(self): - self.assertTrue(compare_chemical_expression( - "5(H1H212)^70010- + 2H20 + 7/2HCl + H2O", - "7/2HCl + 2H20 + H2O + 5(H1H212)^70010-")) - - def test_complex_additivity_wrong(self): - self.assertFalse(compare_chemical_expression( - "5(H1H212)^70010- + 2H20 + 7/2HCl + H2O", - "2H20 + 7/2HCl + H2O + 5(H1H212)^70011-")) - - def test_complex_all_grammar(self): - self.assertTrue(compare_chemical_expression( - "5[Ni(NH3)4]^2+ + 5/2SO4^2-", - "5/2SO4^2- + 5[Ni(NH3)4]^2+")) - - # special cases - - def test_compare_one_superscript_explicitly_set(self): - self.assertTrue(compare_chemical_expression("H^+ + OH^1-", " OH^- + H^+ ")) - - def test_compare_equal_factors_differently_set(self): - self.assertTrue(compare_chemical_expression("6/2H^+ + OH^-", " OH^- + 3H^+ ")) - - def test_compare_one_subscript_explicitly_set(self): - self.assertFalse(compare_chemical_expression("H2 + CO2", "H2 + C102")) - - -class Test_Divide_Equations(unittest.TestCase): - ''' as compare_ use divide_, - tests here must consider different - division (not equality) cases ''' - - def test_divide_wrong_factors(self): - self.assertFalse(divide_chemical_expression( - "5(H1H212)^70010- + 10H2O", "5H2O + 10(H1H212)^70010-")) - - def test_divide_right(self): - self.assertEqual(divide_chemical_expression( - "5(H1H212)^70010- + 10H2O", "10H2O + 5(H1H212)^70010-"), 1) - - def test_divide_wrong_reagents(self): - self.assertFalse(divide_chemical_expression( - "H2O + CO2", "CO2")) - - def test_divide_right_simple(self): - self.assertEqual(divide_chemical_expression( - "H2O + CO2", "H2O+CO2"), 1) - - def test_divide_right_phases(self): - self.assertEqual(divide_chemical_expression( - "H2O(s) + CO2", "2H2O(s)+2CO2"), 2) - - def test_divide_wrong_phases(self): - self.assertFalse(divide_chemical_expression( - "H2O(s) + CO2", "2H2O+2CO2(s)")) - - def test_divide_wrong_phases_but_phases_ignored(self): - self.assertEqual(divide_chemical_expression( - "H2O(s) + CO2", "2H2O+2CO2(s)", ignore_state=True), 2) - - def test_divide_order(self): - self.assertEqual(divide_chemical_expression( - "2CO2 + H2O", "2H2O+4CO2"), 2) - - def test_divide_fract_to_int(self): - self.assertEqual(divide_chemical_expression( - "3/2CO2 + H2O", "2H2O+3CO2"), 2) - - def test_divide_fract_to_frac(self): - self.assertEqual(divide_chemical_expression( - "3/4CO2 + H2O", "2H2O+9/6CO2"), 2) - - def test_divide_fract_to_frac_wrog(self): - self.assertFalse(divide_chemical_expression( - "6/2CO2 + H2O", "2H2O+9/6CO2"), 2) - - -class Test_Render_Equations(unittest.TestCase): - - def test_render1(self): - s = "H2O + CO2" - out = clean_and_render_to_html(s) - correct = "H2O+CO2" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_uncorrect_reaction(self): - s = "O2C + OH2" - out = clean_and_render_to_html(s) - correct = "O2C+OH2" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render2(self): - s = "CO2 + H2O + Fe(OH)3" - out = clean_and_render_to_html(s) - correct = "CO2+H2O+Fe(OH)3" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render3(self): - s = "3H2O + 2CO2" - out = clean_and_render_to_html(s) - correct = "3H2O+2CO2" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render4(self): - s = "H^+ + OH^-" - out = clean_and_render_to_html(s) - correct = "H++OH-" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render5(self): - s = "Fe(OH)^2- + (OH)^-" - out = clean_and_render_to_html(s) - correct = "Fe(OH)2-+(OH)-" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render6(self): - s = "7/2H^+ + 3/5OH^-" - out = clean_and_render_to_html(s) - correct = "72H++35OH-" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render7(self): - s = "5(H1H212)^70010- + 2H2O + 7/2HCl + H2O" - out = clean_and_render_to_html(s) - correct = "5(H1H212)70010-+2H2O+72HCl+H2O" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render8(self): - s = "H2O(s) + CO2" - out = clean_and_render_to_html(s) - correct = "H2O(s)+CO2" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render9(self): - s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-" - #import ipdb; ipdb.set_trace() - out = clean_and_render_to_html(s) - correct = "5[Ni(NH3)4]2++52SO42-" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_error(self): - s = "5.2H20" - self.assertRaises(ParseException, clean_and_render_to_html, s) - - def test_render_simple_brackets(self): - s = "(Ar)" - out = clean_and_render_to_html(s) - correct = "(Ar)" - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - -def suite(): - - testcases = [Test_Compare_Equations, Test_Divide_Equations, Test_Render_Equations] - suites = [] - for testcase in testcases: - suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) - return unittest.TestSuite(suites) - -if __name__ == "__main__": - local_debug = True - with open('render.html', 'w') as f: - unittest.TextTestRunner(verbosity=2).run(suite()) - # open render.html to look at rendered equations + return True diff --git a/common/lib/capa/capa/chem/tests.py b/common/lib/capa/capa/chem/tests.py new file mode 100644 index 0000000000..433fe6feea --- /dev/null +++ b/common/lib/capa/capa/chem/tests.py @@ -0,0 +1,296 @@ +from fractions import Fraction +from pyparsing import ParseException +import unittest + +from chemcalc import (compare_chemical_expression, divide_chemical_expression, + render_to_html, chemical_equations_equal) + +local_debug = None + +def log(s, output_type=None): + if local_debug: + print s + if output_type == 'html': + f.write(s + '\n
\n') + + +class Test_Compare_Equations(unittest.TestCase): + def test_simple_equation(self): + self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + H2 -> H2O2')) + # left sides don't match + self.assertFalse(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + 2H2 -> H2O2')) + # right sides don't match + self.assertFalse(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + H2 -> H2O')) + + # factors don't match + self.assertFalse(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + H2 -> 2H2O2')) + + def test_different_factor(self): + self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', + '2O2 + 2H2 -> 2H2O2')) + + self.assertFalse(chemical_equations_equal('2H2 + O2 -> H2O2', + '2O2 + 2H2 -> 2H2O2')) + + + def test_different_arrows(self): + self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', + '2O2 + 2H2 -> 2H2O2')) + + self.assertFalse(chemical_equations_equal('H2 + O2 -> H2O2', + 'O2 + H2 <-> 2H2O2')) + + + def test_syntax_errors(self): + self.assertRaises(ParseException, chemical_equations_equal, + 'H2 + O2 a-> H2O2', + '2O2 + 2H2 -> 2H2O2') + + self.assertRaises(ParseException, chemical_equations_equal, + 'H2 + O2 ==> H2O2', # strange arrow + '2O2 + 2H2 -> 2H2O2') + + +class Test_Compare_Expressions(unittest.TestCase): + + def test_compare_incorrect_order_of_atoms_in_molecule(self): + self.assertFalse(compare_chemical_expression("H2O + CO2", "O2C + OH2")) + + def test_compare_same_order_no_phases_no_factors_no_ions(self): + self.assertTrue(compare_chemical_expression("H2O + CO2", "CO2+H2O")) + + def test_compare_different_order_no_phases_no_factors_no_ions(self): + self.assertTrue(compare_chemical_expression("H2O + CO2", "CO2 + H2O")) + + def test_compare_different_order_three_multimolecule(self): + self.assertTrue(compare_chemical_expression("H2O + Fe(OH)3 + CO2", "CO2 + H2O + Fe(OH)3")) + + def test_compare_same_factors(self): + self.assertTrue(compare_chemical_expression("3H2O + 2CO2", "2CO2 + 3H2O ")) + + def test_compare_different_factors(self): + self.assertFalse(compare_chemical_expression("2H2O + 3CO2", "2CO2 + 3H2O ")) + + def test_compare_correct_ions(self): + self.assertTrue(compare_chemical_expression("H^+ + OH^-", " OH^- + H^+ ")) + + def test_compare_wrong_ions(self): + self.assertFalse(compare_chemical_expression("H^+ + OH^-", " OH^- + H^- ")) + + def test_compare_parent_groups_ions(self): + self.assertTrue(compare_chemical_expression("Fe(OH)^2- + (OH)^-", " (OH)^- + Fe(OH)^2- ")) + + def test_compare_correct_factors_ions_and_one(self): + self.assertTrue(compare_chemical_expression("3H^+ + 2OH^-", " 2OH^- + 3H^+ ")) + + def test_compare_wrong_factors_ions(self): + self.assertFalse(compare_chemical_expression("2H^+ + 3OH^-", " 2OH^- + 3H^+ ")) + + def test_compare_float_factors(self): + self.assertTrue(compare_chemical_expression("7/2H^+ + 3/5OH^-", " 3/5OH^- + 7/2H^+ ")) + + # Phases tests + def test_compare_phases_ignored(self): + self.assertTrue(compare_chemical_expression( + "H2O(s) + CO2", "H2O+CO2", ignore_state=True)) + + def test_compare_phases_not_ignored_explicitly(self): + self.assertFalse(compare_chemical_expression( + "H2O(s) + CO2", "H2O+CO2", ignore_state=False)) + + def test_compare_phases_not_ignored(self): # same as previous + self.assertFalse(compare_chemical_expression( + "H2O(s) + CO2", "H2O+CO2")) + + def test_compare_phases_not_ignored_explicitly(self): + self.assertTrue(compare_chemical_expression( + "H2O(s) + CO2", "H2O(s)+CO2", ignore_state=False)) + + # all in one cases + def test_complex_additivity(self): + self.assertTrue(compare_chemical_expression( + "5(H1H212)^70010- + 2H20 + 7/2HCl + H2O", + "7/2HCl + 2H20 + H2O + 5(H1H212)^70010-")) + + def test_complex_additivity_wrong(self): + self.assertFalse(compare_chemical_expression( + "5(H1H212)^70010- + 2H20 + 7/2HCl + H2O", + "2H20 + 7/2HCl + H2O + 5(H1H212)^70011-")) + + def test_complex_all_grammar(self): + self.assertTrue(compare_chemical_expression( + "5[Ni(NH3)4]^2+ + 5/2SO4^2-", + "5/2SO4^2- + 5[Ni(NH3)4]^2+")) + + # special cases + + def test_compare_one_superscript_explicitly_set(self): + self.assertTrue(compare_chemical_expression("H^+ + OH^1-", " OH^- + H^+ ")) + + def test_compare_equal_factors_differently_set(self): + self.assertTrue(compare_chemical_expression("6/2H^+ + OH^-", " OH^- + 3H^+ ")) + + def test_compare_one_subscript_explicitly_set(self): + self.assertFalse(compare_chemical_expression("H2 + CO2", "H2 + C102")) + + +class Test_Divide_Expressions(unittest.TestCase): + ''' as compare_ use divide_, + tests here must consider different + division (not equality) cases ''' + + def test_divide_by_zero(self): + self.assertFalse(divide_chemical_expression( + "0H2O", "H2O")) + + def test_divide_wrong_factors(self): + self.assertFalse(divide_chemical_expression( + "5(H1H212)^70010- + 10H2O", "5H2O + 10(H1H212)^70010-")) + + def test_divide_right(self): + self.assertEqual(divide_chemical_expression( + "5(H1H212)^70010- + 10H2O", "10H2O + 5(H1H212)^70010-"), 1) + + def test_divide_wrong_reagents(self): + self.assertFalse(divide_chemical_expression( + "H2O + CO2", "CO2")) + + def test_divide_right_simple(self): + self.assertEqual(divide_chemical_expression( + "H2O + CO2", "H2O+CO2"), 1) + + def test_divide_right_phases(self): + self.assertEqual(divide_chemical_expression( + "H2O(s) + CO2", "2H2O(s)+2CO2"), Fraction(1, 2)) + + def test_divide_right_phases_other_order(self): + self.assertEqual(divide_chemical_expression( + "2H2O(s) + 2CO2", "H2O(s)+CO2"), 2) + + def test_divide_wrong_phases(self): + self.assertFalse(divide_chemical_expression( + "H2O(s) + CO2", "2H2O+2CO2(s)")) + + def test_divide_wrong_phases_but_phases_ignored(self): + self.assertEqual(divide_chemical_expression( + "H2O(s) + CO2", "2H2O+2CO2(s)", ignore_state=True), Fraction(1, 2)) + + def test_divide_order(self): + self.assertEqual(divide_chemical_expression( + "2CO2 + H2O", "2H2O+4CO2"), Fraction(1, 2)) + + def test_divide_fract_to_int(self): + self.assertEqual(divide_chemical_expression( + "3/2CO2 + H2O", "2H2O+3CO2"), Fraction(1, 2)) + + def test_divide_fract_to_frac(self): + self.assertEqual(divide_chemical_expression( + "3/4CO2 + H2O", "2H2O+9/6CO2"), Fraction(1, 2)) + + def test_divide_fract_to_frac_wrog(self): + self.assertFalse(divide_chemical_expression( + "6/2CO2 + H2O", "2H2O+9/6CO2"), 2) + + +class Test_Render_Equations(unittest.TestCase): + + def test_render1(self): + s = "H2O + CO2" + out = render_to_html(s) + correct = "H2O+CO2" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render_uncorrect_reaction(self): + s = "O2C + OH2" + out = render_to_html(s) + correct = "O2C+OH2" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render2(self): + s = "CO2 + H2O + Fe(OH)3" + out = render_to_html(s) + correct = "CO2+H2O+Fe(OH)3" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render3(self): + s = "3H2O + 2CO2" + out = render_to_html(s) + correct = "3H2O+2CO2" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render4(self): + s = "H^+ + OH^-" + out = render_to_html(s) + correct = "H++OH-" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render5(self): + s = "Fe(OH)^2- + (OH)^-" + out = render_to_html(s) + correct = "Fe(OH)2-+(OH)-" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render6(self): + s = "7/2H^+ + 3/5OH^-" + out = render_to_html(s) + correct = "72H++35OH-" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render7(self): + s = "5(H1H212)^70010- + 2H2O + 7/2HCl + H2O" + out = render_to_html(s) + correct = "5(H1H212)70010-+2H2O+72HCl+H2O" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render8(self): + s = "H2O(s) + CO2" + out = render_to_html(s) + correct = "H2O(s)+CO2" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render9(self): + s = "5[Ni(NH3)4]^2+ + 5/2SO4^2-" + #import ipdb; ipdb.set_trace() + out = render_to_html(s) + correct = "5[Ni(NH3)4]2++52SO42-" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + def test_render_error(self): + s = "5.2H20" + self.assertRaises(ParseException, render_to_html, s) + + def test_render_simple_brackets(self): + s = "(Ar)" + out = render_to_html(s) + correct = "(Ar)" + log(out + ' ------- ' + correct, 'html') + self.assertEqual(out, correct) + + +def suite(): + + testcases = [Test_Compare_Expressions, Test_Divide_Expressions, Test_Render_Equations] + suites = [] + for testcase in testcases: + suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) + return unittest.TestSuite(suites) + +if __name__ == "__main__": + local_debug = True + with open('render.html', 'w') as f: + unittest.TextTestRunner(verbosity=2).run(suite()) + # open render.html to look at rendered equations