Skip to content
Projects
Groups
Snippets
Help
This project
Loading...
Sign in / Register
Toggle navigation
E
edx-platform
Overview
Overview
Details
Activity
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
edx
edx-platform
Commits
4b4b2c99
Commit
4b4b2c99
authored
Nov 22, 2012
by
Александр
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
added documentation, tests, and rounding to closes 0.05 value
parent
77c831eb
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
221 additions
and
97 deletions
+221
-97
common/lib/capa/capa/chem/miller.py
+221
-97
No files found.
common/lib/capa/capa/chem/miller.py
View file @
4b4b2c99
""" Calculation of Miller indices """
import
numpy
as
np
import
numpy
as
np
import
math
import
math
import
fractions
as
fr
import
fractions
as
fr
...
@@ -7,150 +9,256 @@ import json
...
@@ -7,150 +9,256 @@ import json
def
lcm
(
a
,
b
):
def
lcm
(
a
,
b
):
""" return lcm of a,b """
"""
Returns least common multiple of a, b
Args:
a, b: floats
Returns:
float
"""
return
a
*
b
/
fr
.
gcd
(
a
,
b
)
return
a
*
b
/
fr
.
gcd
(
a
,
b
)
def
section_to_fraction
(
distance
):
def
segment_to_fraction
(
distance
):
""" Convert float distance, that plane cut on axis
"""
to fraction. Return inverted fraction
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
"""
"""
# import ipdb; ipdb.set_trace()
if
np
.
isnan
(
distance
):
if
np
.
isnan
(
distance
):
# plane || to axis (or contains axis)
print
distance
,
0
# return inverted fration to a == nan == 1/0 => 0 / 1
return
fr
.
Fraction
(
0
,
1
)
return
fr
.
Fraction
(
0
,
1
)
elif
math
.
fabs
(
distance
)
<=
0.05
:
# plane goes through origin, 0.02 - UI delta
return
fr
.
Fraction
(
1
if
distance
>=
0
else
-
1
,
1
)
# ERROR, need shift of coordinates
else
:
else
:
# limit_denominator to closest nicest fraction
fract
=
fr
.
Fraction
(
distance
)
.
limit_denominator
(
10
)
# import ipdb; ipdb.set_trace()
fract
=
fr
.
Fraction
(
distance
)
.
limit_denominator
(
10
)
# 5 / 2 : numerator / denominator
print
'Distance'
,
distance
,
'Inverted fraction'
,
fract
# return inverted fraction
return
fr
.
Fraction
(
fract
.
denominator
,
fract
.
numerator
)
return
fr
.
Fraction
(
fract
.
denominator
,
fract
.
numerator
)
def
sub_miller
(
sections
):
def
sub_miller
(
segments
):
''' Calculate miller indices.
Plane does not intersect origin
'''
'''
fracts
=
[
section_to_fraction
(
section
)
for
section
in
sections
]
Calculates Miller indices from segments.
print
sections
,
fracts
Algorithm:
common_denominator
=
reduce
(
lcm
,
[
fract
.
denominator
for
fract
in
fracts
])
1. Obtain inverted fraction from segments
print
'common_denominator:'
,
common_denominator
2. Find common denominator of inverted fractions
3. Lead fractions to common denominator and throws denominator away.
4. Return obtained values.
# 2) lead to a common denominator
Args:
# 3) throw away denominator
List of 3 floats, meaning distances that plane cuts on x, y, z axes.
miller
=
[
fract
.
numerator
*
math
.
fabs
(
common_denominator
)
/
fract
.
denominator
for
fract
in
fracts
]
Any float not equals zero, it means that plane does not intersect origin,
i. e. shift of origin has already been done.
# import ipdb; ipdb.set_trace()
Returns:
# nice output:
String that represents Miller indices, e.g: (-6,3,-6) or (2,2,2)
# output = '(' + ''.join(map(str, map(decimal.Decimal, miller))) + ')'
'''
# import ipdb; ipdb.set_trace()
fracts
=
[
segment_to_fraction
(
segment
)
for
segment
in
segments
]
output
=
'('
+
','
.
join
(
map
(
str
,
map
(
decimal
.
Decimal
,
miller
)))
+
')'
common_denominator
=
reduce
(
lcm
,
[
fract
.
denominator
for
fract
in
fracts
])
# print 'Miller indices:', output
miller
=
([
fract
.
numerator
*
math
.
fabs
(
common_denominator
)
/
return
output
fract
.
denominator
for
fract
in
fracts
])
return
'('
+
','
.
join
(
map
(
str
,
map
(
decimal
.
Decimal
,
miller
)))
+
')'
def
miller
(
points
):
def
miller
(
points
):
"""Calculate miller indices of plane
"""
"""
Calculates Miller indices from points.
# print "\nCalculating miller indices:"
Algorithm:
# print 'Points:\n', points
# calculate normal to plane
N
=
np
.
cross
(
points
[
1
]
-
points
[
0
],
points
[
2
]
-
points
[
0
])
# print "Normal:", N
# origin
1. Calculate normal vector to a plane that goes trough all points.
O
=
np
.
array
([
0
,
0
,
0
])
2. Set origin.
3. Create Cartesian coordinate system (Ccs).
# point of plane
4. Find the lengths of segments of which the plane cuts the axes. Equation
P
=
points
[
0
]
of a line for axes: Origin + (Coordinate_vector - Origin) * parameter.
# equation of a line for axes: O + (B-O) * t
# t - parameters, B = [Bx, By, Bz]:
B
=
map
(
np
.
array
,
[[
1.0
,
0
,
0
],
[
0
,
1.0
,
0
],
[
0
,
0
,
1.0
]])
# coordinates of intersections with axis:
sections
=
[
np
.
dot
(
P
-
O
,
N
)
/
np
.
dot
(
B_axis
,
N
)
if
np
.
dot
(
B_axis
,
N
)
!=
0
else
np
.
nan
for
B_axis
in
B
]
# import ipdb; ipdb.set_trace()
if
any
(
x
==
0
for
x
in
sections
):
# Plane goes through origin.
# Need to shift plane out of origin.
# For this change origin position
# 1) find cube vertex, not crossed by plane
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
]),
]
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
:
for
vertex
in
vertices
:
if
np
.
dot
(
vertex
-
O
,
N
)
!=
0
:
# vertex in plane
if
np
.
dot
(
vertex
-
O
,
N
)
!=
0
:
# vertex
not
in plane
new_origin
=
vertex
new_origin
=
vertex
break
break
# obtain new axes with center in new origin
# get axis with center in new origin
X
=
np
.
array
([
1
-
new_origin
[
0
],
new_origin
[
1
],
new_origin
[
2
]])
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
]])
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
]])
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
# 2) calculate miller indexes by new origin
return
sub_miller
(
segments
)
new_axis
=
[
X
-
new_origin
,
Y
-
new_origin
,
Z
-
new_origin
]
# coordinates of intersections with axis:
sections
=
[
np
.
dot
(
P
-
new_origin
,
N
)
/
np
.
dot
(
B_axis
,
N
)
if
np
.
dot
(
B_axis
,
N
)
!=
0
else
np
.
nan
for
B_axis
in
new_axis
]
# 3) fix signs of indexes
# 0 -> 1, 1 -> -1
sections
=
(
1
-
2
*
new_origin
)
*
sections
return
sub_miller
(
sections
)
def
grade
(
user_input
,
correct_answer
):
def
grade
(
user_input
,
correct_answer
):
'''
'''
Format:
Grade crystallography problem.
user_input: {"lattice":"sc","points":[["0.77","0.00","1.00"],["0.78","1.00","0.00"],["0.00","1.00","0.72"]]}
"lattice" is one of: "", "sc", "bcc", "fcc"
Returns true if lattices are the same and Miller indices are same or minus
correct_answer: {'miller': '(00-1)', 'lattice': 'bcc'}
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
(
s
):
def
negative
(
m
):
# import ipdb; ipdb.set_trace()
"""
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
=
''
output
=
''
i
=
1
i
=
1
while
i
in
range
(
1
,
len
(
s
)
-
1
):
while
i
in
range
(
1
,
len
(
m
)
-
1
):
if
s
[
i
]
in
(
','
,
' '
):
if
m
[
i
]
in
(
','
,
' '
):
output
+=
s
[
i
]
output
+=
m
[
i
]
elif
s
[
i
]
not
in
(
'-'
,
'0'
):
elif
m
[
i
]
not
in
(
'-'
,
'0'
):
output
+=
'-'
+
s
[
i
]
output
+=
'-'
+
m
[
i
]
elif
s
[
i
]
==
'0'
:
elif
m
[
i
]
==
'0'
:
output
+=
s
[
i
]
output
+=
m
[
i
]
else
:
else
:
i
+=
1
i
+=
1
output
+=
s
[
i
]
output
+=
m
[
i
]
i
+=
1
i
+=
1
# import ipdb; ipdb.set_trace()
return
'('
+
output
+
')'
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
)
user_answer
=
json
.
loads
(
user_input
)
if
user_answer
[
'lattice'
]
!=
correct_answer
[
'lattice'
]:
if
user_answer
[
'lattice'
]
!=
correct_answer
[
'lattice'
]:
return
False
return
False
points
=
[
map
(
float
,
p
)
for
p
in
user_answer
[
'points'
]]
points
=
[
map
(
float
,
p
)
for
p
in
user_answer
[
'points'
]]
# round point to closes 0.05 value
points
=
[
round0_25
(
point
)
for
point
in
points
]
points
=
[
np
.
array
(
point
)
for
point
in
points
]
points
=
[
np
.
array
(
point
)
for
point
in
points
]
# import ipdb; ipdb.set_trace()
print
miller
(
points
),
(
correct_answer
[
'miller'
]
.
replace
(
' '
,
''
),
print
miller
(
points
),
(
correct_answer
[
'miller'
]
.
replace
(
' '
,
''
),
negative
(
correct_answer
[
'miller'
])
.
replace
(
' '
,
''
))
negative
(
correct_answer
[
'miller'
])
.
replace
(
' '
,
''
))
if
miller
(
points
)
in
(
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
True
...
@@ -159,7 +267,7 @@ def grade(user_input, correct_answer):
...
@@ -159,7 +267,7 @@ def grade(user_input, correct_answer):
class
Test_Crystallography_Miller
(
unittest
.
TestCase
):
class
Test_Crystallography_Miller
(
unittest
.
TestCase
):
'''
test crystallography grade function
'''
'''
Tests for crystallography grade function.
'''
def
test_1
(
self
):
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"]]}'
user_input
=
'{"lattice": "bcc", "points": [["0.50", "0.00", "0.00"], ["0.00", "0.50", "0.00"], ["0.00", "0.00", "0.50"]]}'
...
@@ -178,8 +286,10 @@ class Test_Crystallography_Miller(unittest.TestCase):
...
@@ -178,8 +286,10 @@ class Test_Crystallography_Miller(unittest.TestCase):
self
.
assertTrue
(
grade
(
user_input
,
{
'miller'
:
'(-3, 3, -3)'
,
'lattice'
:
'bcc'
}))
self
.
assertTrue
(
grade
(
user_input
,
{
'miller'
:
'(-3, 3, -3)'
,
'lattice'
:
'bcc'
}))
def
test_5
(
self
):
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"]]}'
user_input
=
'{"lattice": "bcc", "points": [["0.33", "1.00", "0.00"], ["0.00", "0.33", "0.00"], ["0.00", "1.00", "0.33"]]}'
self
.
assert
Tru
e
(
grade
(
user_input
,
{
'miller'
:
'(-6,3,-6)'
,
'lattice'
:
'bcc'
}))
self
.
assert
Fals
e
(
grade
(
user_input
,
{
'miller'
:
'(-6,3,-6)'
,
'lattice'
:
'bcc'
}))
def
test_6
(
self
):
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"]]}'
user_input
=
'{"lattice": "bcc", "points": [["0.00", "0.25", "0.00"], ["0.25", "0.00", "0.00"], ["0.00", "0.00", "0.25"]]}'
...
@@ -261,6 +371,20 @@ class Test_Crystallography_Miller(unittest.TestCase):
...
@@ -261,6 +371,20 @@ class Test_Crystallography_Miller(unittest.TestCase):
user_input
=
u'{"lattice":"","points":[["0.00","0.00","0.01"],["1.00","1.00","0.01"],["0.00","1.00","1.00"]]}'
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
(
grade
(
user_input
,
{
'miller'
:
'(1,-1,1)'
,
'lattice'
:
''
}))
self
.
assertTrue
(
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
(
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
(
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
(
grade
(
user_input
,
{
'miller'
:
'(10,10,10)'
,
'lattice'
:
''
}))
def
test_wrong_lattice
(
self
):
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"]]}'
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
(
grade
(
user_input
,
{
'miller'
:
'(3,3,3)'
,
'lattice'
:
'fcc'
}))
self
.
assertFalse
(
grade
(
user_input
,
{
'miller'
:
'(3,3,3)'
,
'lattice'
:
'fcc'
}))
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment