Remove local copy of openedx-chem package
now that we install it from an external repository.
This commit is contained in:
@@ -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(" <sup>{num}</sup>⁄<sub>{den}</sub> ").format(num=tree[0][0], den=tree[2][0])
|
||||
return "Error"
|
||||
|
||||
def subscript(tree, children):
|
||||
return HTML("<sub>{sub}</sub>").format(sub=children)
|
||||
|
||||
def superscript(tree, children):
|
||||
return HTML("<sup>{sup}</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 <span>.
|
||||
"""
|
||||
def err(s):
|
||||
"""
|
||||
Render as an error span
|
||||
"""
|
||||
return HTML('<span class="inline-error inline">{0}</span>').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'<span class="math">{0}</span>').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
|
||||
@@ -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())
|
||||
@@ -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
|
||||
@@ -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<br>\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'<span class="math">H<sub>2</sub>O+CO<sub>2</sub></span>'
|
||||
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'<span class="math">O<sub>2</sub>C+OH<sub>2</sub></span>'
|
||||
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'<span class="math">CO<sub>2</sub>+H<sub>2</sub>O+Fe(OH)<sub>3</sub></span>'
|
||||
log(out + ' ------- ' + correct, 'html')
|
||||
self.assertEqual(out, correct)
|
||||
|
||||
def test_render3(self):
|
||||
test_string = "3H2O + 2CO2"
|
||||
out = render_to_html(test_string)
|
||||
correct = u'<span class="math">3H<sub>2</sub>O+2CO<sub>2</sub></span>'
|
||||
log(out + ' ------- ' + correct, 'html')
|
||||
self.assertEqual(out, correct)
|
||||
|
||||
def test_render4(self):
|
||||
test_string = "H^+ + OH^-"
|
||||
out = render_to_html(test_string)
|
||||
correct = u'<span class="math">H<sup>+</sup>+OH<sup>-</sup></span>'
|
||||
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'<span class="math">Fe(OH)<sup>2-</sup>+(OH)<sup>-</sup></span>'
|
||||
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'<span class="math"><sup>7</sup>⁄<sub>2</sub>H<sup>+</sup>+<sup>3</sup>⁄<sub>5</sub>OH<sup>-</sup></span>'
|
||||
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'<span class="math">5(H<sub>1</sub>H<sub>212</sub>)<sup>70010-</sup>+2H<sub>2</sub>O+<sup>7</sup>⁄<sub>2</sub>HCl+H<sub>2</sub>O</span>'
|
||||
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'<span class="math">H<sub>2</sub>O(s)+CO<sub>2</sub></span>'
|
||||
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'<span class="math">5[Ni(NH<sub>3</sub>)<sub>4</sub>]<sup>2+</sup>+<sup>5</sup>⁄<sub>2</sub>SO<sub>4</sub><sup>2-</sup></span>'
|
||||
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'<span class="math"><span class="inline-error inline">5.2H20</span></span>'
|
||||
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'<span class="math">(Ar)</span>'
|
||||
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'<span class="math">[Ar]</span>'
|
||||
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'<span class="math">H<sup>+</sup>+OH<sup>-</sup>\u2192H<sub>2</sub>O</span>'
|
||||
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'<span class="math">H<sup>+</sup>+OH<sup>-</sup>\u2194H<sub>2</sub>O</span>'
|
||||
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'<span class="math"><span class="inline-error inline">H^+ + OH^- <= H2O</span></span>'
|
||||
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'<span class="math">[H<sup>+</sup>]+OH<sup>-</sup>\u2194(H<sub>2</sub>O)</span>'
|
||||
log(out + ' ------- ' + correct, 'html')
|
||||
self.assertEqual(out, correct)
|
||||
|
||||
def test_escaping(self):
|
||||
"""
|
||||
Tests that invalid input is escaped.
|
||||
"""
|
||||
test_string = "<script>f()</script>"
|
||||
out = render_to_html(test_string)
|
||||
correct = u'<span class="math"><span class="inline-error inline"><script>f()</script></span></span>'
|
||||
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
|
||||
@@ -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.
|
||||
],
|
||||
)
|
||||
@@ -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',
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user