diff --git a/common/lib/capa/capa/capa_problem.py b/common/lib/capa/capa/capa_problem.py index 451891d067..db42fb698a 100644 --- a/common/lib/capa/capa/capa_problem.py +++ b/common/lib/capa/capa/capa_problem.py @@ -33,6 +33,7 @@ from xml.sax.saxutils import unescape import chem import chem.chemcalc import chem.chemtools +import chem.miller import calc from correctmap import CorrectMap @@ -67,7 +68,8 @@ global_context = {'random': random, 'calc': calc, 'eia': eia, 'chemcalc': chem.chemcalc, - 'chemtools': chem.chemtools} + 'chemtools': chem.chemtools, + 'miller': chem.miller} # These should be removed from HTML output, including all subelements html_problem_semantics = ["codeparam", "responseparam", "answer", "script", "hintgroup"] diff --git a/common/lib/capa/capa/chem/miller.py b/common/lib/capa/capa/chem/miller.py new file mode 100644 index 0000000000..4c10e60ecc --- /dev/null +++ b/common/lib/capa/capa/chem/miller.py @@ -0,0 +1,267 @@ +""" Calculation of Miller indices """ + +import numpy as np +import math +import fractions as fr +import decimal +import json + + +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 recieve 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 = ([fract.numerator * math.fabs(common_denominator) / + fract.denominator for fract in fracts]) + return'(' + ','.join(map(str, map(decimal.Decimal, miller))) + ')' + + +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 = 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 = [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/capa/capa/chem/tests.py b/common/lib/capa/capa/chem/tests.py index 34d903ec1d..571526f915 100644 --- a/common/lib/capa/capa/chem/tests.py +++ b/common/lib/capa/capa/chem/tests.py @@ -1,13 +1,15 @@ import codecs from fractions import Fraction -from pyparsing import ParseException import unittest from chemcalc import (compare_chemical_expression, divide_chemical_expression, render_to_html, chemical_equations_equal) +import miller + local_debug = None + def log(s, output_type=None): if local_debug: print s @@ -37,7 +39,6 @@ class Test_Compare_Equations(unittest.TestCase): 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')) @@ -56,7 +57,6 @@ class Test_Compare_Equations(unittest.TestCase): 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')) @@ -311,7 +311,6 @@ class Test_Render_Equations(unittest.TestCase): log(out + ' ------- ' + correct, 'html') self.assertEqual(out, correct) - def test_render_eq3(self): s = "H^+ + OH^- <= H2O" # unsupported arrow out = render_to_html(s) @@ -320,10 +319,148 @@ class Test_Render_Equations(unittest.TestCase): self.assertEqual(out, correct) +class Test_Crystallography_Miller(unittest.TestCase): + ''' Tests for crystallography grade function.''' + + def test_empty_points(self): + user_input = '{"lattice": "bcc", "points": []}' + self.assertFalse(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(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(miller.grade(user_input, {'miller': '(3,3,3)', 'lattice': 'fcc'})) + def suite(): - testcases = [Test_Compare_Expressions, Test_Divide_Expressions, Test_Render_Equations] + testcases = [Test_Compare_Expressions, + Test_Divide_Expressions, + Test_Render_Equations, + Test_Crystallography_Miller] suites = [] for testcase in testcases: suites.append(unittest.TestLoader().loadTestsFromTestCase(testcase)) diff --git a/common/lib/capa/capa/inputtypes.py b/common/lib/capa/capa/inputtypes.py index 0b2250f98d..ec1cda83c7 100644 --- a/common/lib/capa/capa/inputtypes.py +++ b/common/lib/capa/capa/inputtypes.py @@ -671,18 +671,15 @@ class Crystallography(InputTypeBase): """ Note: height, width are required. """ - return [Attribute('size', None), - Attribute('height'), + return [Attribute('height'), Attribute('width'), - - # can probably be removed (textline should prob be always-hidden) - Attribute('hidden', ''), ] registry.register(Crystallography) # ------------------------------------------------------------------------- + class VseprInput(InputTypeBase): """ Input for molecular geometry--show possible structures, let student diff --git a/common/lib/capa/capa/templates/crystallography.html b/common/lib/capa/capa/templates/crystallography.html index 2370f59dd2..8dcbff354b 100644 --- a/common/lib/capa/capa/templates/crystallography.html +++ b/common/lib/capa/capa/templates/crystallography.html @@ -1,34 +1,28 @@
-
+
+ +
+ Lattice: +
+
-
- % if status == 'unsubmitted': -
+
% elif status == 'correct': -
+
% elif status == 'incorrect': -
+
% elif status == 'incomplete': -
- % endif - % if hidden: -
+
% endif - -

+ + +

% if status == 'unsubmitted': unanswered % elif status == 'correct': @@ -38,14 +32,15 @@ % elif status == 'incomplete': incomplete % endif -

+

-

+

- % if msg: - ${msg|n} - % endif -% if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: -
-% endif + % if msg: + ${msg|n} + % endif + + % if status in ['unsubmitted', 'correct', 'incorrect', 'incomplete']: +
+ % endif
diff --git a/common/lib/capa/capa/tests/test_inputtypes.py b/common/lib/capa/capa/tests/test_inputtypes.py index 826d304717..dafd31bdc7 100644 --- a/common/lib/capa/capa/tests/test_inputtypes.py +++ b/common/lib/capa/capa/tests/test_inputtypes.py @@ -407,13 +407,11 @@ class CrystallographyTest(unittest.TestCase): def test_rendering(self): height = '12' width = '33' - size = '10' xml_str = """""".format(h=height, w=width, s=size) + />""".format(h=height, w=width) element = etree.fromstring(xml_str) @@ -428,9 +426,7 @@ class CrystallographyTest(unittest.TestCase): expected = {'id': 'prob_1_2', 'value': value, 'status': 'unsubmitted', - 'size': size, 'msg': '', - 'hidden': '', 'width': width, 'height': height, } diff --git a/docs/source/capa.rst b/docs/source/capa.rst index f83d89f52d..345855af5e 100644 --- a/docs/source/capa.rst +++ b/docs/source/capa.rst @@ -1,9 +1,15 @@ ******************************************* Capa module ******************************************* +Contents: .. module:: capa +.. toctree:: + :maxdepth: 2 + + chem.rst + Calc ==== diff --git a/docs/source/chem.rst b/docs/source/chem.rst new file mode 100644 index 0000000000..26e01a3238 --- /dev/null +++ b/docs/source/chem.rst @@ -0,0 +1,69 @@ +******************************************* +Chem module +******************************************* + +.. module:: chem + +Miller +====== + +.. automodule:: capa.chem.miller + :members: + :show-inheritance: + +UI part and inputtypes +---------------------- +Miller module is used in the system in crystallography problems. +Crystallography is a class in :mod:`capa` inputtypes module. +It uses *crystallography.html* for rendering and **crystallography.js** +for UI part. + +Documentation from **crystallography.js**:: + + For a crystallographic problem of the type + + Given a plane definition via miller indexes, specify it by plotting points on the edges + of a 3D cube. Additionally, select the correct Bravais cubic lattice type depending on the + physical crystal mentioned in the problem. + + we create a graph which contains a cube, and a 3D Cartesian coordinate system. The interface + will allow to plot 3 points anywhere along the edges of the cube, and select which type of + Bravais lattice should be displayed along with the basic cube outline. + + When 3 points are successfully plotted, an intersection of the resulting plane (defined by + the 3 plotted points), and the cube, will be automatically displayed for clarity. + + After lotting the three points, it is possible to continue plotting additional points. By + doing so, the point that was plotted first (from the three that already exist), will be + removed, and the new point will be added. The intersection of the resulting new plane and + the cube will be redrawn. + + The UI has been designed in such a way, that the user is able to determine which point will + be removed next (if adding a new point). This is achieved via filling the to-be-removed point + with a different color. + + + +Chemcalc +======== + +.. automodule:: capa.chem.chemcalc + :members: + :show-inheritance: + +Chemtools +========= + +.. automodule:: capa.chem.chemtools + :members: + :show-inheritance: + + +Tests +===== + +.. automodule:: capa.chem.tests + :members: + :show-inheritance: + +