Skip to content

Commit e146398

Browse files
committed
initial commit of morphology shape classification NO_JIRA
1 parent 600305c commit e146398

File tree

2 files changed

+234
-0
lines changed

2 files changed

+234
-0
lines changed
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
# Classification of shape of crystal morphology
2+
3+
Add text here
Lines changed: 231 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,231 @@
1+
#
2+
# This script can be used for any purpose without limitation subject to the
3+
# conditions at http://www.ccdc.cam.ac.uk/Community/Pages/Licences/v2.aspx
4+
#
5+
# This permission notice and the following statement of attribution must be
6+
# included in all copies or substantial portions of this script.
7+
#
8+
9+
"""
10+
Script to classify the shape of a crystal morphology as belonging to one of four classes:
11+
12+
- block
13+
- plate
14+
- lath
15+
- needle
16+
17+
The shape classification follows the definition of Angelidakis, Nadimi and Utili found in
18+
Powder Technology, 396 (2022), 689-695 DOI: 10.1016/j.powtec.2021.11.027
19+
20+
"""
21+
22+
import numpy as np
23+
import matplotlib.pyplot as plt
24+
from matplotlib import cm
25+
from cycler import cycler
26+
27+
28+
def _elongation_line():
29+
"""
30+
This function's purpose is exclusively that of calculating the lines that separate different regions of the plot.
31+
Shape classification is not based on this function.
32+
33+
Finds the line corresponding to aspect ratios with elongation = 0.2
34+
"""
35+
36+
# set b = 1, then solve elongation = 0.2 for a using a solver
37+
# ac/(ac+1)-c/(a+c) = 0.2
38+
39+
values = np.linspace(0.0001, 1, 100)
40+
41+
def func(c):
42+
# these are the solutions of the equation to get a as a function of c
43+
sol1 = (c ** 2 + 1 - np.sqrt(c ** 4 + 98 * c ** 2 + 1)) / (8 * c)
44+
sol2 = (c ** 2 + 1 + np.sqrt(c ** 4 + 98 * c ** 2 + 1)) / (8 * c)
45+
return sol1, sol2
46+
47+
# get the "elongation" line
48+
x_values = []
49+
y_values = []
50+
for c in values:
51+
x_values.append(c)
52+
# we keep positive solutions only
53+
_, a = func(c)
54+
y_values.append(1 / a)
55+
# x_values are the c/b ratios
56+
# y_values are the b/a ratios
57+
return x_values, y_values
58+
59+
60+
def _flatness_line():
61+
"""
62+
This function's purpose is exclusively that of calculating the lines that separate different regions of the plot.
63+
Shape classification is not based on this function.
64+
65+
Finds the line corresponding to aspect ratios with flatness = 0.2
66+
"""
67+
# set a = 1, then solve flatness = 0.2 for c using a solver
68+
# b^2/(c+b^2) - c/(c+1) = 0.2
69+
values = np.linspace(0.0001, 1, 100)
70+
71+
def func(b):
72+
# these are the solutions of the equation to get c as a function of b
73+
sol1 = (- b ** 2 - 1 + np.sqrt(b ** 4 + 98 * b ** 2 + 1)) / 12
74+
sol2 = (- b ** 2 + 1 + np.sqrt(b ** 4 + 98 * b ** 2 + 1)) / 12
75+
return sol1, sol2
76+
77+
# get the "flatness" line
78+
x_values = []
79+
y_values = []
80+
for b in values:
81+
y_values.append(b)
82+
# we keep positive solutions only
83+
c, _ = func(b)
84+
x_values.append(c / b)
85+
# x_values are the c/b ratios
86+
# y_values are the b/a ratios
87+
return x_values, y_values
88+
89+
90+
def plot(shape):
91+
"""
92+
Prepare a Zingg plot for a single morphology.
93+
94+
:param shape: an instance of the ShapeClassifier class
95+
"""
96+
fig, ax = plt.subplots()
97+
ax.set_xlim(0, 1)
98+
ax.set_ylim(0, 1)
99+
ax.set_aspect("equal")
100+
ax.set_xlabel("S / M")
101+
ax.set_ylabel("M / L")
102+
ax.text(0.01, 0.95, "PLATE")
103+
ax.text(0.85, 0.01, "NEEDLE")
104+
ax.text(0.85, 0.95, "BLOCK")
105+
ax.text(0.2, 0.2, "LATH")
106+
107+
# calculate the dividing lines
108+
# line where elongation = 0.2
109+
el_line = elongation_line()
110+
# line where flatness = 0.2
111+
fl_line = flatness_line()
112+
ax.plot(el_line[0], el_line[1], lw=1, c="k")
113+
ax.plot(fl_line[0], fl_line[1], lw=1, c="k")
114+
# plot lines of classic Zingg plot
115+
ax.axvline(x=2 / 3, c="grey", lw=0.5, ls="--")
116+
ax.axhline(y=2 / 3, c="grey", lw=0.5, ls="--")
117+
# finally plot the data
118+
# can add additional arguments here to make prettier
119+
ax.scatter(self.minor_length / self.median_length, self.median_length / self.major_length)
120+
plt.show()
121+
122+
123+
def plot_multiple_shapes(shapes):
124+
"""
125+
Make a Zingg plo for multiple morphologies.
126+
127+
:param shapes: a list of ShapeClassifier objects
128+
"""
129+
130+
plt.rcParams.update({'font.family': 'Arial'})
131+
fig, ax = plt.subplots()
132+
ax.set_xlim(0, 1)
133+
ax.set_ylim(0, 1)
134+
ax.set_aspect("equal")
135+
ax.set_xlabel("S / M")
136+
ax.set_ylabel("M / L")
137+
ax.text(0.01, 0.95, "PLATE")
138+
ax.text(0.85, 0.01, "NEEDLE")
139+
ax.text(0.85, 0.95, "BLOCK")
140+
ax.text(0.2, 0.2, "LATH")
141+
142+
# calculate the dividing lines
143+
# line where elongation = 0.2
144+
el_line = _elongation_line()
145+
# line where flatness = 0.2
146+
fl_line = _flatness_line()
147+
ax.plot(el_line[0], el_line[1], lw=1, c="k")
148+
ax.plot(fl_line[0], fl_line[1], lw=1, c="k")
149+
# plot lines of classic Zingg plot
150+
ax.axvline(x=2 / 3, c="grey", lw=0.5, ls="--")
151+
ax.axhline(y=2 / 3, c="grey", lw=0.5, ls="--")
152+
# define a colormap
153+
colours = cm.bwr(np.linspace(0, 1, len([item for item in shapes if item != 0])))
154+
# finally plot the data
155+
ax.set_prop_cycle(cycler('color', colours))
156+
for shape in shapes:
157+
ax.scatter(shape.minor_length / shape.median_length, shape.median_length / shape.major_length)
158+
plt.show()
159+
160+
161+
class ShapeClassifier:
162+
163+
def __init__(self, morphology: object):
164+
"""
165+
param morphology: an instance of ccdc.morphology classes
166+
"""
167+
self.morphology = morphology
168+
self.bounding_box = self.morphology.oriented_bounding_box
169+
self.major_length, self.median_length, self.minor_length = self.bounding_box.lengths
170+
171+
@property
172+
def elongation(self) -> float:
173+
return (self.major_length * self.minor_length) / (
174+
self.major_length * self.minor_length + self.median_length ** 2) - self.minor_length / (
175+
self.major_length + self.minor_length)
176+
177+
@property
178+
def flatness(self) -> float:
179+
return self.median_length ** 2 / (
180+
self.major_length * self.minor_length + self.median_length ** 2) - self.minor_length / (
181+
self.major_length + self.minor_length)
182+
183+
@property
184+
def compactness(self) -> float:
185+
return 2 * self.minor_length / (self.major_length + self.minor_length)
186+
187+
@property
188+
def shape_description(self) -> str:
189+
"""Return the classification of the shape"""
190+
if self.elongation >= 0.2:
191+
if self.flatness >= 0.2:
192+
return "Lath"
193+
else:
194+
return "Needle"
195+
if self.elongation < 0.2:
196+
if self.flatness < 0.2:
197+
return "Block"
198+
else:
199+
return "Plate"
200+
201+
202+
if __name__ == "__main__":
203+
# an example of how to use this script
204+
205+
from ccdc.io import EntryReader
206+
from ccdc.morphology import BFDHMorphology
207+
208+
# let's calculate a morphology!
209+
refcode = "VUKRAW"
210+
reader = EntryReader("CSD")
211+
entry = reader.entry(refcode)
212+
crystal = entry.crystal
213+
214+
bfdh_morphology = BFDHMorphology(crystal)
215+
216+
shape_1 = ShapeClassifier(bfdh_morphology)
217+
print(f"{refcode} BFDH morphology is classified as a: {shape_1.shape_description}")
218+
219+
# let's try with another morphology!
220+
refcode = "AABHTZ"
221+
reader = EntryReader("CSD")
222+
entry = reader.entry(refcode)
223+
crystal = entry.crystal
224+
225+
bfdh_morphology = BFDHMorphology(crystal)
226+
227+
shape_2 = ShapeClassifier(bfdh_morphology)
228+
print(f"{refcode} BFDH morphology is classified as a: {shape_2.shape_description}")
229+
230+
# now let's make a Zingg plot to visualise our particles' shapes
231+
plot_multiple_shapes([shape_1, shape_2])

0 commit comments

Comments
 (0)