diff --git a/common/lib/chem/chem/__init__.py b/common/lib/chem/chem/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/common/lib/chem/chem/chemcalc.py b/common/lib/chem/chem/chemcalc.py deleted file mode 100644 index 81c71177ea..0000000000 --- a/common/lib/chem/chem/chemcalc.py +++ /dev/null @@ -1,459 +0,0 @@ -from __future__ import absolute_import, division -from fractions import Fraction - -import markupsafe -import nltk -from nltk.tree import Tree -from pyparsing import Literal, OneOrMore, ParseException, StringEnd -from six.moves import map -from six.moves import range -from six.moves import zip -from functools import reduce - - -ARROWS = ('<->', '->') - -# Defines a simple pyparsing tokenizer for chemical equations -elements = ['Ac', 'Ag', 'Al', 'Am', 'Ar', 'As', 'At', 'Au', 'B', 'Ba', 'Be', - 'Bh', 'Bi', 'Bk', 'Br', 'C', 'Ca', 'Cd', 'Ce', 'Cf', 'Cl', 'Cm', - 'Cn', 'Co', 'Cr', 'Cs', 'Cu', 'Db', 'Ds', 'Dy', 'Er', 'Es', 'Eu', - 'F', 'Fe', 'Fl', 'Fm', 'Fr', 'Ga', 'Gd', 'Ge', 'H', 'He', 'Hf', - 'Hg', 'Ho', 'Hs', 'I', 'In', 'Ir', 'K', 'Kr', 'La', 'Li', 'Lr', - 'Lu', 'Lv', 'Md', 'Mg', 'Mn', 'Mo', 'Mt', 'N', 'Na', 'Nb', 'Nd', - 'Ne', 'Ni', 'No', 'Np', 'O', 'Os', 'P', 'Pa', 'Pb', 'Pd', 'Pm', - 'Po', 'Pr', 'Pt', 'Pu', 'Ra', 'Rb', 'Re', 'Rf', 'Rg', 'Rh', 'Rn', - 'Ru', 'S', 'Sb', 'Sc', 'Se', 'Sg', 'Si', 'Sm', 'Sn', 'Sr', 'Ta', - 'Tb', 'Tc', 'Te', 'Th', 'Ti', 'Tl', 'Tm', 'U', 'Uuo', 'Uup', - 'Uus', 'Uut', 'V', 'W', 'Xe', 'Y', 'Yb', 'Zn', 'Zr'] -digits = list(map(str, list(range(10)))) -symbols = list("[](){}^+-/") -phases = ["(s)", "(l)", "(g)", "(aq)"] -tokens = reduce(lambda a, b: a ^ b, list(map(Literal, elements + digits + symbols + phases))) -tokenizer = OneOrMore(tokens) + StringEnd() - -# HTML, Text are temporarily copied from openedx.core.djangolib.markup -# These libraries need to be moved out of edx-platform to be used by -# other applications. -# See LEARNER-5853 for more details. -Text = markupsafe.escape # pylint: disable=invalid-name - - -def HTML(html): # pylint: disable=invalid-name - return markupsafe.Markup(html) - - -def _orjoin(l): - return "'" + "' | '".join(l) + "'" - - -# 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) + """ - number -> digit | digit number - group -> suffixed | suffixed group - paren_group_round -> '(' group ')' - paren_group_square -> '[' group ']' - plus_minus -> '+' | '-' - number_suffix -> number - ion_suffix -> '^' number plus_minus | '^' plus_minus - suffix -> number_suffix | number_suffix ion_suffix | ion_suffix - unsuffixed -> element | paren_group_round | paren_group_square - - suffixed -> unsuffixed | unsuffixed suffix -""" -parser = nltk.ChartParser(nltk.CFG.fromstring(grammar)) - - -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. - """ - def unparse_number(n): - """ - Go from a number parse tree to a number - """ - if len(n) == 1: - rv = n[0][0] - else: - rv = n[0][0] + unparse_number(n[1]) - return rv - - def null_tag(n): - """ - Remove a tag - """ - return n[0] - - def ion_suffix(n): - """ - 1. "if" part handles special case - 2. "else" part is general behaviour - """ - if n[1:][0].label() == 'number' and n[1:][0][0][0] == '1': - # if suffix is explicitly 1, like ^1- - # strip 1, leave only sign: ^- - return nltk.tree.Tree(n.label(), n[2:]) - else: - return nltk.tree.Tree(n.label(), n[1:]) - - dispatch = {'number': lambda x: nltk.tree.Tree("number", [unparse_number(x)]), - 'unphased': null_tag, - 'unsuffixed': null_tag, - 'number_suffix': lambda x: nltk.tree.Tree('number_suffix', [unparse_number(x[0])]), - 'suffixed': lambda x: len(x) > 1 and x or x[0], - 'ion_suffix': ion_suffix, - 'paren_group_square': lambda x: nltk.tree.Tree(x.label(), x[1]), - 'paren_group_round': lambda x: nltk.tree.Tree(x.label(), x[1])} - - if isinstance(tree, str): - return tree - - old_node = None - # This loop means that if a node is processed, and returns a child, - # the child will be processed. - while tree.label() in dispatch and tree.label() != old_node: - old_node = tree.label() - tree = dispatch[tree.label()](tree) - - children = [] - for child in tree: - child = _clean_parse_tree(child) - children.append(child) - - tree = nltk.tree.Tree(tree.label(), children) - - return tree - - -def _merge_children(tree, tags): - """ - nltk, by documentation, cannot do arbitrary length groups. - Instead of: (group 1 2 3 4) - It has to handle this recursively: (group 1 (group 2 (group 3 (group 4)))) - We do the cleanup of converting from the latter to the former. - """ - if tree is None: - # There was a problem--shouldn't have empty trees (NOTE: see this with input e.g. 'H2O(', or 'Xe+'). - raise ParseException("Shouldn't have empty trees") - - if isinstance(tree, str): - return tree - - merged_children = [] - done = False - - # Merge current tag - while not done: - done = True - for child in tree: - if isinstance(child, nltk.tree.Tree) and child.label() == tree.label() and tree.label() in tags: - merged_children = merged_children + list(child) - done = False - else: - merged_children = merged_children + [child] - tree = nltk.tree.Tree(tree.label(), merged_children) - merged_children = [] - - # And recurse - children = [] - for child in tree: - children.append(_merge_children(child, tags)) - - return nltk.tree.Tree(tree.label(), children) - - -def _render_to_html(tree): - """ - Renders a cleaned tree to HTML - """ - def molecule_count(tree, children): - # If an integer, return that integer - if len(tree) == 1: - return tree[0][0] - # If a fraction, return the fraction - if len(tree) == 3: - return HTML(" {num}{den} ").format(num=tree[0][0], den=tree[2][0]) - return "Error" - - def subscript(tree, children): - return HTML("{sub}").format(sub=children) - - def superscript(tree, children): - return HTML("{sup}").format(sup=children) - - def round_brackets(tree, children): - return HTML("({insider})").format(insider=children) - - def square_brackets(tree, children): - return HTML("[{insider}]").format(insider=children) - - dispatch = {'count': molecule_count, - 'number_suffix': subscript, - 'ion_suffix': superscript, - 'paren_group_round': round_brackets, - 'paren_group_square': square_brackets} - - if isinstance(tree, str): - return tree - else: - children = HTML("").join(map(_render_to_html, tree)) - if tree.label() in dispatch: - return dispatch[tree.label()](tree, children) - else: - return children.replace(' ', '') - - -def render_to_html(eq): - """ - Render a chemical equation string to html. - - Renders each molecule separately, and returns invalid input wrapped in a . - """ - def err(s): - """ - Render as an error span - """ - return HTML('{0}').format(s) - - def render_arrow(arrow): - """ - Turn text arrows into pretty ones - """ - if arrow == '->': - return HTML(u'\u2192') - if arrow == '<->': - return HTML(u'\u2194') - - # this won't be reached unless we add more arrow types, but keep it to avoid explosions when - # that happens. HTML-escape this unknown arrow just in case. - return Text(arrow) - - def render_expression(ex): - """ - Render a chemical expression--no arrows. - """ - try: - return _render_to_html(_get_final_tree(ex)) - except ParseException: - return err(ex) - - def spanify(s): - return HTML(u'{0}').format(s) - - left, arrow, right = split_on_arrow(eq) - if arrow == '': - # only one side - return spanify(render_expression(left)) - - return spanify(render_expression(left) + render_arrow(arrow) + render_expression(right)) - - -def _get_final_tree(s): - """ - Return final tree after merge and clean. - - Raises pyparsing.ParseException if s is invalid. - """ - try: - tokenized = tokenizer.parseString(s) - parsed = parser.parse(tokenized) - merged = _merge_children(next(parsed), {'S', 'group'}) - final = _clean_parse_tree(merged) - return final - except StopIteration: - # This happens with an empty tree- see this with input e.g. 'H2O(', or 'Xe+'). - raise ParseException("Shouldn't have empty trees") - - -def _check_equality(tuple1, tuple2): - """ - return True if tuples of multimolecules are equal - """ - list1 = list(tuple1) - list2 = list(tuple2) - - # Hypo: trees where are levels count+molecule vs just molecule - # cannot be sorted properly (tested on test_complex_additivity) - # But without factors and phases sorting seems to work. - - # Also for lists of multimolecules without factors and phases - # sorting seems to work fine. - list1.sort() - list2.sort() - return list1 == list2 - - -def compare_chemical_expression(s1, s2, ignore_state=False): - """ - It does comparison between two expressions. - It uses divide_chemical_expression and check if division is 1 - """ - return divide_chemical_expression(s1, s2, ignore_state) == 1 - - -def divide_chemical_expression(s1, s2, ignore_state=False): - """ - Compare two chemical expressions 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 expressions 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 - - """ - - # parsed final trees - treedic = { - '1': _get_final_tree(s1), - '2': _get_final_tree(s2) - } - - # strip phases and factors - # collect factors in list - for i in ('1', '2'): - treedic[i + ' cleaned_mm_list'] = [] - treedic[i + ' factors'] = [] - treedic[i + ' phases'] = [] - for el in treedic[i].subtrees(filter=lambda t: t.label() == 'multimolecule'): - count_subtree = [t for t in el.subtrees() if t.label() == 'count'] - group_subtree = [t for t in el.subtrees() if t.label() == 'group'] - phase_subtree = [t for t in el.subtrees() if t.label() == 'phase'] - if count_subtree: - if len(count_subtree[0]) > 1: - treedic[i + ' factors'].append( - int(count_subtree[0][0][0]) / - int(count_subtree[0][2][0])) - else: - treedic[i + ' factors'].append(int(count_subtree[0][0][0])) - else: - treedic[i + ' factors'].append(1.0) - if phase_subtree: - treedic[i + ' phases'].append(phase_subtree[0][0]) - else: - treedic[i + ' phases'].append(' ') - treedic[i + ' cleaned_mm_list'].append( - Tree('multimolecule', [Tree('molecule', group_subtree)])) - - # order of factors and phases must mirror the order of multimolecules, - # use 'decorate, sort, undecorate' pattern - treedic['1 cleaned_mm_list'], treedic['1 factors'], treedic['1 phases'] = list(zip( - *sorted(zip(treedic['1 cleaned_mm_list'], treedic['1 factors'], treedic['1 phases'])))) - - treedic['2 cleaned_mm_list'], treedic['2 factors'], treedic['2 phases'] = list(zip( - *sorted(zip(treedic['2 cleaned_mm_list'], treedic['2 factors'], treedic['2 phases'])))) - - # check if expressions are correct without factors - if not _check_equality(treedic['1 cleaned_mm_list'], treedic['2 cleaned_mm_list']): - return False - - # phases are ruled by ingore_state flag - if not ignore_state: # phases matters - if treedic['1 phases'] != treedic['2 phases']: - return False - - if any( - [ - x / y - treedic['1 factors'][0] / treedic['2 factors'][0] - for (x, y) in zip(treedic['1 factors'], treedic['2 factors']) - ] - ): - # factors are not proportional - return False - else: - # return ratio - return Fraction(treedic['1 factors'][0] / treedic['2 factors'][0]) - - -def split_on_arrow(eq): - """ - Split a string on an arrow. Returns left, arrow, right. If there is no arrow, returns the - entire eq in left, and '' in arrow and right. - - Return left, arrow, right. - """ - # order matters -- need to try <-> first - for arrow in ARROWS: - left, a, right = eq.partition(arrow) - if a != '': - return left, a, right - - return eq, '', '' - - -def chemical_equations_equal(eq1, eq2, exact=False): - """ - Check whether two chemical equations are the same. (equations have arrows) - - If exact is False, then they are considered equal if they differ by a - constant factor. - - arrows matter: -> and <-> are different. - - e.g. - chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + H2 -> H2O2') -> True - chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + 2H2 -> H2O2') -> False - - chemical_equations_equal('H2 + O2 -> H2O2', 'O2 + H2 <-> H2O2') -> False - - chemical_equations_equal('H2 + O2 -> H2O2', '2 H2 + 2 O2 -> 2 H2O2') -> True - chemical_equations_equal('H2 + O2 -> H2O2', '2 H2 + 2 O2 -> 2 H2O2', exact=True) -> False - - - If there's a syntax error, we return False. - """ - - left1, arrow1, right1 = split_on_arrow(eq1) - left2, arrow2, right2 = split_on_arrow(eq2) - - if arrow1 == '' or arrow2 == '': - return False - - # 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 - - try: - factor_left = divide_chemical_expression(left1, left2) - if not factor_left: - # left sides don't match - return False - - factor_right = divide_chemical_expression(right1, right2) - if not factor_right: - # right sides don't match - return False - - if factor_left != factor_right: - # factors don't match (molecule counts to add up) - return False - - if exact and factor_left != 1: - # want an exact match. - return False - - return True - except ParseException: - # Don't want external users to have to deal with parsing exceptions. Just return False. - return False diff --git a/common/lib/chem/chem/chemtools.py b/common/lib/chem/chem/chemtools.py deleted file mode 100644 index 19405bdcad..0000000000 --- a/common/lib/chem/chem/chemtools.py +++ /dev/null @@ -1,207 +0,0 @@ -"""This module originally includes functions for grading Vsepr problems. - -Also, may be this module is the place for other chemistry-related grade functions. TODO: discuss it. -""" - -from __future__ import absolute_import -import itertools -import json -import unittest - - -def vsepr_parse_user_answer(user_input): - """ - user_input is json generated by vsepr.js from dictionary. - There are must be only two keys in original user_input dictionary: "geometry" and "atoms". - Format: u'{"geometry": "AX3E0","atoms":{"c0": "B","p0": "F","p1": "B","p2": "F"}}' - Order of elements inside "atoms" subdict does not matters. - Return dict from parsed json. - - "Atoms" subdict stores positions of atoms in molecule. - General types of positions: - c0 - central atom - p0..pN - peripheral atoms - a0..aN - axial atoms - e0..eN - equatorial atoms - - Each position is dictionary key, i.e. user_input["atoms"]["c0"] is central atom, user_input["atoms"]["a0"] is one of axial atoms. - - Special position only for AX6 (Octahedral) geometry: - e10, e12 - atom pairs opposite the central atom, - e20, e22 - atom pairs opposite the central atom, - e1 and e2 pairs lying crosswise in equatorial plane. - - In user_input["atoms"] may be only 3 set of keys: - (c0,p0..pN), - (c0, a0..aN, e0..eN), - (c0, a0, a1, e10,e11,e20,e21) - if geometry is AX6. - """ - return json.loads(user_input) - - -def vsepr_build_correct_answer(geometry, atoms): - """ - geometry is string. - atoms is dict of atoms with proper positions. - Example: - - correct_answer = vsepr_build_correct_answer(geometry="AX4E0", atoms={"c0": "N", "p0": "H", "p1": "(ep)", "p2": "H", "p3": "H"}) - - returns a dictionary composed from input values: - {'geometry': geometry, 'atoms': atoms} - """ - return {'geometry': geometry, 'atoms': atoms} - - -def vsepr_grade(user_input, correct_answer, convert_to_peripheral=False): - """ - This function does comparison between user_input and correct_answer. - - Comparison is successful if all steps are successful: - - 1) geometries are equal - 2) central atoms (index in dictionary 'c0') are equal - 3): - In next steps there is comparing of corresponding subsets of atom positions: equatorial (e0..eN), axial (a0..aN) or peripheral (p0..pN) - - If convert_to_peripheral is True, then axial and equatorial positions are converted to peripheral. - This means that user_input from: - "atoms":{"c0": "Br","a0": "test","a1": "(ep)","e10": "H","e11": "(ep)","e20": "H","e21": "(ep)"}}' after parsing to json - is converted to: - {"c0": "Br", "p0": "(ep)", "p1": "test", "p2": "H", "p3": "H", "p4": "(ep)", "p6": "(ep)"} - i.e. aX and eX -> pX - - So if converted, p subsets are compared, - if not a and e subsets are compared - If all subsets are equal, grade succeeds. - - There is also one special case for AX6 geometry. - In this case user_input["atoms"] contains special 3 symbol keys: e10, e12, e20, and e21. - Correct answer for this geometry can be of 3 types: - 1) c0 and peripheral - 2) c0 and axial and equatorial - 3) c0 and axial and equatorial-subset-1 (e1X) and equatorial-subset-2 (e2X) - - If correct answer is type 1 or 2, then user_input is converted from type 3 to type 2 (or to type 1 if convert_to_peripheral is True) - - If correct_answer is type 3, then we done special case comparison. We have 3 sets of atoms positions both in user_input and correct_answer: axial, eq-1 and eq-2. - Answer will be correct if these sets are equals for one of permutations. For example, if : - user_axial = correct_eq-1 - user_eq-1 = correct-axial - user_eq-2 = correct-eq-2 - - """ - if user_input['geometry'] != correct_answer['geometry']: - return False - - if user_input['atoms']['c0'] != correct_answer['atoms']['c0']: - return False - - if convert_to_peripheral: - # convert user_input from (a,e,e1,e2) to (p) - # correct_answer must be set in (p) using this flag - c0 = user_input['atoms'].pop('c0') - user_input['atoms'] = {'p' + str(i): v for i, v in enumerate(user_input['atoms'].values())} - user_input['atoms']['c0'] = c0 - - # special case for AX6 - if 'e10' in correct_answer['atoms']: # need check e1x, e2x symmetry for AX6.. - a_user = {} - a_correct = {} - for ea_position in ['a', 'e1', 'e2']: # collecting positions: - a_user[ea_position] = [v for k, v in user_input['atoms'].items() if k.startswith(ea_position)] - a_correct[ea_position] = [v for k, v in correct_answer['atoms'].items() if k.startswith(ea_position)] - - correct = [sorted(a_correct['a'])] + [sorted(a_correct['e1'])] + [sorted(a_correct['e2'])] - for permutation in itertools.permutations(['a', 'e1', 'e2']): - if correct == [sorted(a_user[permutation[0]])] + [sorted(a_user[permutation[1]])] + [sorted(a_user[permutation[2]])]: - return True - return False - - else: # no need to check e1x,e2x symmetry - convert them to ex - if 'e10' in user_input['atoms']: # e1x exists, it is AX6.. case - e_index = 0 - for k, v in user_input['atoms'].items(): - if len(k) == 3: # e1x - del user_input['atoms'][k] - user_input['atoms']['e' + str(e_index)] = v - e_index += 1 - - # common case - for ea_position in ['p', 'a', 'e']: - # collecting atoms: - a_user = [v for k, v in user_input['atoms'].items() if k.startswith(ea_position)] - a_correct = [v for k, v in correct_answer['atoms'].items() if k.startswith(ea_position)] - # print a_user, a_correct - if len(a_user) != len(a_correct): - return False - if sorted(a_user) != sorted(a_correct): - return False - - return True - - -class Test_Grade(unittest.TestCase): - ''' test grade function ''' - - def test_incorrect_geometry(self): - correct_answer = vsepr_build_correct_answer(geometry="AX4E0", atoms={"c0": "N", "p0": "H", "p1": "(ep)", "p2": "H", "p3": "H"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX3E0","atoms":{"c0": "B","p0": "F","p1": "B","p2": "F"}}') - self.assertFalse(vsepr_grade(user_answer, correct_answer)) - - def test_correct_answer_p(self): - correct_answer = vsepr_build_correct_answer(geometry="AX4E0", atoms={"c0": "N", "p0": "H", "p1": "(ep)", "p2": "H", "p3": "H"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX4E0","atoms":{"c0": "N","p0": "H","p1": "(ep)","p2": "H", "p3": "H"}}') - self.assertTrue(vsepr_grade(user_answer, correct_answer)) - - def test_correct_answer_ae(self): - correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "test", "a1": "(ep)", "e0": "H", "e1": "H", "e2": "(ep)", "e3": "(ep)"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "test","a1": "(ep)","e10": "H","e11": "H","e20": "(ep)","e21": "(ep)"}}') - self.assertTrue(vsepr_grade(user_answer, correct_answer)) - - def test_correct_answer_ae_convert_to_p_but_input_not_in_p(self): - correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "test", "e0": "H", "e1": "H", "e2": "(ep)", "e3": "(ep)"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "test","a1": "(ep)","e10": "H","e11": "(ep)","e20": "H","e21": "(ep)"}}') - self.assertFalse(vsepr_grade(user_answer, correct_answer, convert_to_peripheral=True)) - - def test_correct_answer_ae_convert_to_p(self): - correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "p0": "(ep)", "p1": "test", "p2": "H", "p3": "H", "p4": "(ep)", "p6": "(ep)"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "test","a1": "(ep)","e10": "H","e11": "(ep)","e20": "H","e21": "(ep)"}}') - self.assertTrue(vsepr_grade(user_answer, correct_answer, convert_to_peripheral=True)) - - def test_correct_answer_e1e2_in_a(self): - correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "(ep)", "e10": "H", "e11": "H", "e20": "H", "e21": "H"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "(ep)","a1": "(ep)","e10": "H","e11": "H","e20": "H","e21": "H"}}') - self.assertTrue(vsepr_grade(user_answer, correct_answer)) - - def test_correct_answer_e1e2_in_e1(self): - correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "(ep)", "e10": "H", "e11": "H", "e20": "H", "e21": "H"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "H","a1": "H","e10": "(ep)","e11": "(ep)","e20": "H","e21": "H"}}') - self.assertTrue(vsepr_grade(user_answer, correct_answer)) - - def test_correct_answer_e1e2_in_e2(self): - correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "(ep)", "e10": "H", "e11": "H", "e20": "H", "e21": "H"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "H","a1": "H","e10": "H","e11": "H","e20": "(ep)","e21": "(ep)"}}') - self.assertTrue(vsepr_grade(user_answer, correct_answer)) - - def test_incorrect_answer_e1e2(self): - correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "(ep)", "e10": "H", "e11": "H", "e20": "H", "e21": "H"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "Br","a0": "H","a1": "H","e10": "(ep)","e11": "H","e20": "H","e21": "(ep)"}}') - self.assertFalse(vsepr_grade(user_answer, correct_answer)) - - def test_incorrect_c0(self): - correct_answer = vsepr_build_correct_answer(geometry="AX6E0", atoms={"c0": "Br", "a0": "(ep)", "a1": "test", "e0": "H", "e1": "H", "e2": "H", "e3": "(ep)"}) - user_answer = vsepr_parse_user_answer(u'{"geometry": "AX6E0","atoms":{"c0": "H","a0": "test","a1": "(ep)","e0": "H","e1": "H","e2": "(ep)","e3": "H"}}') - self.assertFalse(vsepr_grade(user_answer, correct_answer)) - - -def suite(): - - testcases = [Test_Grade] - suites = [] - for testcase in testcases: - suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) - return unittest.TestSuite(suites) - -if __name__ == "__main__": - unittest.TextTestRunner(verbosity=2).run(suite()) diff --git a/common/lib/chem/chem/miller.py b/common/lib/chem/chem/miller.py deleted file mode 100644 index c0b5868cf4..0000000000 --- a/common/lib/chem/chem/miller.py +++ /dev/null @@ -1,277 +0,0 @@ -""" Calculation of Miller indices """ - -from __future__ import absolute_import -import decimal -import fractions as fr -import json -import math - -import numpy as np -from six.moves import map -from six.moves import range -from functools import reduce - - -def lcm(a, b): - """ - Returns least common multiple of a, b - - Args: - a, b: floats - - Returns: - float - """ - return a * b / fr.gcd(a, b) - - -def segment_to_fraction(distance): - """ - Converts lengths of which the plane cuts the axes to fraction. - - Tries convert distance to closest nicest fraction with denominator less or - equal than 10. It is - purely for simplicity and clearance of learning purposes. Jenny: 'In typical - courses students usually do not encounter indices any higher than 6'. - - If distance is not a number (numpy nan), it means that plane is parallel to - axis or contains it. Inverted fraction to nan (nan is 1/0) = 0 / 1 is - returned - - Generally (special cases): - - a) if distance is smaller than some constant, i.g. 0.01011, - than fraction's denominator usually much greater than 10. - - b) Also, if student will set point on 0.66 -> 1/3, so it is 333 plane, - But if he will slightly move the mouse and click on 0.65 -> it will be - (16,15,16) plane. That's why we are doing adjustments for points coordinates, - to the closest tick, tick + tick / 2 value. And now UI sends to server only - values multiple to 0.05 (half of tick). Same rounding is implemented for - unittests. - - But if one will want to calculate miller indices with exact coordinates and - with nice fractions (which produce small Miller indices), he may want shift - to new origin if segments are like S = (0.015, > 0.05, >0.05) - close to zero - in one coordinate. He may update S to (0, >0.05, >0.05) and shift origin. - In this way he can receive nice small fractions. Also there is can be - degenerated case when S = (0.015, 0.012, >0.05) - if update S to (0, 0, >0.05) - - it is a line. This case should be considered separately. Small nice Miller - numbers and possibility to create very small segments can not be implemented - at same time). - - - Args: - distance: float distance that plane cuts on axis, it must not be 0. - Distance is multiple of 0.05. - - Returns: - Inverted fraction. - 0 / 1 if distance is nan - - """ - if np.isnan(distance): - return fr.Fraction(0, 1) - else: - fract = fr.Fraction(distance).limit_denominator(10) - return fr.Fraction(fract.denominator, fract.numerator) - - -def sub_miller(segments): - ''' - Calculates Miller indices from segments. - - Algorithm: - - 1. Obtain inverted fraction from segments - - 2. Find common denominator of inverted fractions - - 3. Lead fractions to common denominator and throws denominator away. - - 4. Return obtained values. - - Args: - List of 3 floats, meaning distances that plane cuts on x, y, z axes. - Any float not equals zero, it means that plane does not intersect origin, - i. e. shift of origin has already been done. - - Returns: - String that represents Miller indices, e.g: (-6,3,-6) or (2,2,2) - ''' - fracts = [segment_to_fraction(segment) for segment in segments] - common_denominator = reduce(lcm, [fract.denominator for fract in fracts]) - miller_indices = ([ - fract.numerator * math.fabs(common_denominator) / fract.denominator - for fract in fracts - ]) - return'(' + ','.join(map(str, list(map(decimal.Decimal, miller_indices)))) + ')' - - -def miller(points): - """ - Calculates Miller indices from points. - - Algorithm: - - 1. Calculate normal vector to a plane that goes trough all points. - - 2. Set origin. - - 3. Create Cartesian coordinate system (Ccs). - - 4. Find the lengths of segments of which the plane cuts the axes. Equation - of a line for axes: Origin + (Coordinate_vector - Origin) * parameter. - - 5. If plane goes trough Origin: - - a) Find new random origin: find unit cube vertex, not crossed by a plane. - - b) Repeat 2-4. - - c) Fix signs of segments after Origin shift. This means to consider - original directions of axes. I.g.: Origin was 0,0,0 and became - new_origin. If new_origin has same Y coordinate as Origin, then segment - does not change its sign. But if new_origin has another Y coordinate than - origin (was 0, became 1), than segment has to change its sign (it now - lies on negative side of Y axis). New Origin 0 value of X or Y or Z - coordinate means that segment does not change sign, 1 value -> does - change. So new sign is (1 - 2 * new_origin): 0 -> 1, 1 -> -1 - - 6. Run function that calculates miller indices from segments. - - Args: - List of points. Each point is list of float coordinates. Order of - coordinates in point's list: x, y, z. Points are different! - - Returns: - String that represents Miller indices, e.g: (-6,3,-6) or (2,2,2) - """ - - N = np.cross(points[1] - points[0], points[2] - points[0]) - O = np.array([0, 0, 0]) - P = points[0] # point of plane - Ccs = list(map(np.array, [[1.0, 0, 0], [0, 1.0, 0], [0, 0, 1.0]])) - segments = ([ - np.dot(P - O, N) / np.dot(ort, N) if np.dot(ort, N) != 0 - else np.nan for ort in Ccs - ]) - if any(x == 0 for x in segments): # Plane goes through origin. - vertices = [ - # top: - np.array([1.0, 1.0, 1.0]), - np.array([0.0, 0.0, 1.0]), - np.array([1.0, 0.0, 1.0]), - np.array([0.0, 1.0, 1.0]), - # bottom, except 0,0,0: - np.array([1.0, 0.0, 0.0]), - np.array([0.0, 1.0, 0.0]), - np.array([1.0, 1.0, 1.0]), - ] - for vertex in vertices: - if np.dot(vertex - O, N) != 0: # vertex not in plane - new_origin = vertex - break - # obtain new axes with center in new origin - X = np.array([1 - new_origin[0], new_origin[1], new_origin[2]]) - Y = np.array([new_origin[0], 1 - new_origin[1], new_origin[2]]) - Z = np.array([new_origin[0], new_origin[1], 1 - new_origin[2]]) - new_Ccs = [X - new_origin, Y - new_origin, Z - new_origin] - segments = ([np.dot(P - new_origin, N) / np.dot(ort, N) if - np.dot(ort, N) != 0 else np.nan for ort in new_Ccs]) - # fix signs of indices: 0 -> 1, 1 -> -1 ( - segments = (1 - 2 * new_origin) * segments - - return sub_miller(segments) - - -def grade(user_input, correct_answer): - ''' - Grade crystallography problem. - - Returns true if lattices are the same and Miller indices are same or minus - same. E.g. (2,2,2) = (2, 2, 2) or (-2, -2, -2). Because sign depends only - on student's selection of origin. - - Args: - user_input, correct_answer: json. Format: - - user_input: {"lattice":"sc","points":[["0.77","0.00","1.00"], - ["0.78","1.00","0.00"],["0.00","1.00","0.72"]]} - - correct_answer: {'miller': '(00-1)', 'lattice': 'bcc'} - - "lattice" is one of: "", "sc", "bcc", "fcc" - - Returns: - True or false. - ''' - def negative(m): - """ - Change sign of Miller indices. - - Args: - m: string with meaning of Miller indices. E.g.: - (-6,3,-6) -> (6, -3, 6) - - Returns: - String with changed signs. - """ - output = '' - i = 1 - while i in range(1, len(m) - 1): - if m[i] in (',', ' '): - output += m[i] - elif m[i] not in ('-', '0'): - output += '-' + m[i] - elif m[i] == '0': - output += m[i] - else: - i += 1 - output += m[i] - i += 1 - return '(' + output + ')' - - def round0_25(point): - """ - Rounds point coordinates to closest 0.5 value. - - Args: - point: list of float coordinates. Order of coordinates: x, y, z. - - Returns: - list of coordinates rounded to closes 0.5 value - """ - rounded_points = [] - for coord in point: - base = math.floor(coord * 10) - fractional_part = (coord * 10 - base) - aliquot0_25 = math.floor(fractional_part / 0.25) - if aliquot0_25 == 0.0: - rounded_points.append(base / 10) - if aliquot0_25 in (1.0, 2.0): - rounded_points.append(base / 10 + 0.05) - if aliquot0_25 == 3.0: - rounded_points.append(base / 10 + 0.1) - return rounded_points - - user_answer = json.loads(user_input) - - if user_answer['lattice'] != correct_answer['lattice']: - return False - - points = [list(map(float, p)) for p in user_answer['points']] - - if len(points) < 3: - return False - - # round point to closes 0.05 value - points = [round0_25(point) for point in points] - - points = [np.array(point) for point in points] - # print miller(points), (correct_answer['miller'].replace(' ', ''), - # negative(correct_answer['miller']).replace(' ', '')) - if miller(points) in (correct_answer['miller'].replace(' ', ''), negative(correct_answer['miller']).replace(' ', '')): - return True - - return False diff --git a/common/lib/chem/chem/tests.py b/common/lib/chem/chem/tests.py deleted file mode 100644 index 848859e6ff..0000000000 --- a/common/lib/chem/chem/tests.py +++ /dev/null @@ -1,504 +0,0 @@ -from __future__ import absolute_import, print_function -import codecs -import unittest -from fractions import Fraction - -import chem.miller - -from .chemcalc import chemical_equations_equal, compare_chemical_expression, divide_chemical_expression, render_to_html - -LOCAL_DEBUG = None - - -def log(msg, output_type=None): - """Logging function for tests""" - if LOCAL_DEBUG: - print(msg) - if output_type == 'html': - f.write(msg + '\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_exact_match(self): - self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', - '2O2 + 2H2 -> 2H2O2')) - - self.assertFalse( - chemical_equations_equal( - 'H2 + O2 -> H2O2', - '2O2 + 2H2 -> 2H2O2', - exact=True, - ) - ) - - # order still doesn't matter - self.assertTrue(chemical_equations_equal('H2 + O2 -> H2O2', - 'O2 + H2 -> H2O2', exact=True)) - - def test_syntax_errors(self): - self.assertFalse(chemical_equations_equal('H2 + O2 a-> H2O2', - '2O2 + 2H2 -> 2H2O2')) - - self.assertFalse(chemical_equations_equal('H2O( -> H2O2', - 'H2O -> H2O2')) - - self.assertFalse(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")) - - # 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): - """ - Tests to validate the HTML rendering of plaintext (input) equations - """ - # pylint: disable=line-too-long - def test_render1(self): - test_string = "H2O + CO2" - out = render_to_html(test_string) - correct = u'H2O+CO2' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_uncorrect_reaction(self): - test_string = "O2C + OH2" - out = render_to_html(test_string) - correct = u'O2C+OH2' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render2(self): - test_string = "CO2 + H2O + Fe(OH)3" - out = render_to_html(test_string) - correct = u'CO2+H2O+Fe(OH)3' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render3(self): - test_string = "3H2O + 2CO2" - out = render_to_html(test_string) - correct = u'3H2O+2CO2' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render4(self): - test_string = "H^+ + OH^-" - out = render_to_html(test_string) - correct = u'H++OH-' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render5(self): - test_string = "Fe(OH)^2- + (OH)^-" - out = render_to_html(test_string) - correct = u'Fe(OH)2-+(OH)-' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render6(self): - test_string = "7/2H^+ + 3/5OH^-" - out = render_to_html(test_string) - correct = u'72H++35OH-' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render7(self): - test_string = "5(H1H212)^70010- + 2H2O + 7/2HCl + H2O" - out = render_to_html(test_string) - correct = u'5(H1H212)70010-+2H2O+72HCl+H2O' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render8(self): - test_string = "H2O(s) + CO2" - out = render_to_html(test_string) - correct = u'H2O(s)+CO2' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render9(self): - test_string = "5[Ni(NH3)4]^2+ + 5/2SO4^2-" - out = render_to_html(test_string) - correct = u'5[Ni(NH3)4]2++52SO42-' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_error(self): - test_string = "5.2H20" - out = render_to_html(test_string) - correct = u'5.2H20' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_simple_round_brackets(self): - test_string = "(Ar)" - out = render_to_html(test_string) - correct = u'(Ar)' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_simple_square_brackets(self): - test_string = "[Ar]" - out = render_to_html(test_string) - correct = u'[Ar]' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_eq1(self): - test_string = "H^+ + OH^- -> H2O" - out = render_to_html(test_string) - correct = u'H++OH-\u2192H2O' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_eq2(self): - test_string = "H^+ + OH^- <-> H2O" - out = render_to_html(test_string) - correct = u'H++OH-\u2194H2O' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_eq3(self): - test_string = "H^+ + OH^- <= H2O" # unsupported arrow - out = render_to_html(test_string) - correct = u'H^+ + OH^- <= H2O' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_render_eq4(self): - test_string = "[H^+] + OH^- <-> (H2O)" # with brackets - out = render_to_html(test_string) - correct = u'[H+]+OH-\u2194(H2O)' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - def test_escaping(self): - """ - Tests that invalid input is escaped. - """ - test_string = "" - out = render_to_html(test_string) - correct = u'<script>f()</script>' - log(out + ' ------- ' + correct, 'html') - self.assertEqual(out, correct) - - -class Test_Crystallography_Miller(unittest.TestCase): - """Tests for crystallography grade function.""" - # pylint: disable=line-too-long - def test_empty_points(self): - user_input = '{"lattice": "bcc", "points": []}' - self.assertFalse(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_only_one_point(self): - user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"]]}' - self.assertFalse(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_only_two_points(self): - user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"]]}' - self.assertFalse(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_1(self): - user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"], ["0.00", "0.00", "0.50"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_2(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,1,1)', 'lattice': 'bcc'})) - - def test_3(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.50", "1.00"], ["1.00", "1.00", "0.50"], ["0.50", "1.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(2,2,2)', 'lattice': 'bcc'})) - - def test_4(self): - user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.664", "0.00"], ["0.00", "1.00", "0.33"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(-3, 3, -3)', 'lattice': 'bcc'})) - - def test_5(self): - """ return true only in case points coordinates are exact. - But if they transform to closest 0.05 value it is not true""" - user_input = '{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.33", "0.00"], ["0.00", "1.00", "0.33"]]}' - self.assertFalse(chem.miller.grade(user_input, {'miller': '(-6,3,-6)', 'lattice': 'bcc'})) - - def test_6(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.25", "0.00"], ["0.25", "0.00", "0.00"], ["0.00", "0.00", "0.25"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(4,4,4)', 'lattice': 'bcc'})) - - def test_7(self): # goes throug origin - user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "0.00", "0.00"], ["0.50", "1.00", "0.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,0,-1)', 'lattice': 'bcc'})) - - def test_8(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.50"], ["1.00", "0.00", "0.50"], ["0.50", "1.00", "0.50"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,0,2)', 'lattice': 'bcc'})) - - def test_9(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "1.00", "1.00"], ["1.00", "0.00", "0.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,1,0)', 'lattice': 'bcc'})) - - def test_10(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "1.00"], ["0.00", "0.00", "0.00"], ["0.00", "1.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'})) - - def test_11(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "0.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,1,2)', 'lattice': 'bcc'})) - - def test_12(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.50"], ["0.00", "0.00", "0.50"], ["1.00", "1.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,1,-2)', 'lattice': 'bcc'})) - - def test_13(self): - user_input = '{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.50", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(2,0,1)', 'lattice': 'bcc'})) - - def test_14(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "0.00", "1.00"], ["0.50", "1.00", "0.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(2,-1,0)', 'lattice': 'bcc'})) - - def test_15(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'})) - - def test_16(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "1.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,1,-1)', 'lattice': 'bcc'})) - - def test_17(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "1.00"], ["1.00", "1.00", "0.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(-1,1,1)', 'lattice': 'bcc'})) - - def test_18(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "1.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': 'bcc'})) - - def test_19(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(-1,1,0)', 'lattice': 'bcc'})) - - def test_20(self): - user_input = '{"lattice": "bcc", "points": [["1.00", "0.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,0,1)', 'lattice': 'bcc'})) - - def test_21(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["0.00", "1.00", "0.00"], ["1.00", "0.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(-1,0,1)', 'lattice': 'bcc'})) - - def test_22(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "1.00", "0.00"], ["1.00", "1.00", "0.00"], ["0.00", "0.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,1,1)', 'lattice': 'bcc'})) - - def test_23(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,-1,1)', 'lattice': 'bcc'})) - - def test_24(self): - user_input = '{"lattice": "bcc", "points": [["0.66", "0.00", "0.00"], ["0.00", "0.66", "0.00"], ["0.00", "0.00", "0.66"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'bcc'})) - - def test_25(self): - user_input = u'{"lattice":"","points":[["0.00","0.00","0.01"],["1.00","1.00","0.01"],["0.00","1.00","1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(1,-1,1)', 'lattice': ''})) - - def test_26(self): - user_input = u'{"lattice":"","points":[["0.00","0.01","0.00"],["1.00","0.00","0.00"],["0.00","0.00","1.00"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(0,-1,0)', 'lattice': ''})) - - def test_27(self): - """ rounding to 0.35""" - user_input = u'{"lattice":"","points":[["0.33","0.00","0.00"],["0.00","0.33","0.00"],["0.00","0.00","0.33"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': ''})) - - def test_28(self): - """ rounding to 0.30""" - user_input = u'{"lattice":"","points":[["0.30","0.00","0.00"],["0.00","0.30","0.00"],["0.00","0.00","0.30"]]}' - self.assertTrue(chem.miller.grade(user_input, {'miller': '(10,10,10)', 'lattice': ''})) - - def test_wrong_lattice(self): - user_input = '{"lattice": "bcc", "points": [["0.00", "0.00", "0.00"], ["1.00", "0.00", "0.00"], ["1.00", "1.00", "1.00"]]}' - self.assertFalse(chem.miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'fcc'})) - - -def suite(): - - testcases = [Test_Compare_Expressions, - Test_Divide_Expressions, - Test_Render_Equations, - Test_Crystallography_Miller] - suites = [] - for testcase in testcases: - suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) - return unittest.TestSuite(suites) - -if __name__ == "__main__": - LOCAL_DEBUG = True - with codecs.open('render.html', 'w', encoding='utf-8') as f: - unittest.TextTestRunner(verbosity=2).run(suite()) - # open render.html to look at rendered equations diff --git a/common/lib/chem/setup.py b/common/lib/chem/setup.py deleted file mode 100644 index d297ce9217..0000000000 --- a/common/lib/chem/setup.py +++ /dev/null @@ -1,15 +0,0 @@ -from __future__ import absolute_import -from setuptools import setup - -setup( - name="chem", - version="0.3.0", - packages=["chem"], - install_requires=[ - "pyparsing==2.2.0", - "numpy", - "scipy", - "nltk", - "markupsafe", # Should be replaced by other utilities. See LEARNER-5853 for more details. - ], -) diff --git a/docs/conf.py b/docs/conf.py index a3c8bcd0e3..685d68818b 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -27,7 +27,6 @@ sys.path.append(root / "docs") sys.path.append(root / "cms/djangoapps") sys.path.append(root / "common/djangoapps") sys.path.append(root / "common/lib/capa") -sys.path.append(root / "common/lib/chem") sys.path.append(root / "common/lib/safe_lxml") sys.path.append(root / "common/lib/symmath") sys.path.append(root / "common/lib/xmodule") @@ -236,7 +235,6 @@ autodoc_mock_imports = [ modules = { 'cms': 'cms', 'common/lib/capa/capa': 'common/lib/capa', - 'common/lib/chem/chem': 'common/lib/chem', 'common/lib/safe_lxml/safe_lxml': 'common/lib/safe_lxml', 'common/lib/symmath/symmath': 'common/lib/symmath', 'common/lib/xmodule/xmodule': 'common/lib/xmodule', diff --git a/docs/docstrings/common_lib.rst b/docs/docstrings/common_lib.rst index c1cc729fab..419063bd5b 100644 --- a/docs/docstrings/common_lib.rst +++ b/docs/docstrings/common_lib.rst @@ -9,7 +9,6 @@ out from edx-platform into separate packages at some point. :maxdepth: 2 common/lib/capa/modules - common/lib/chem/modules common/lib/safe_lxml/modules common/lib/symmath/modules common/lib/xmodule/modules