################################################################################
# This Python 3 code generates a series of MCCs of type (m+a)_a
################################################################################
# Source code accompanying the manuscript
#    "Magic Number Colloidal Clusters as Minimum Free Energy Structures"
# written 2016-2018 by Michael Engel and Junwei Wang
#
# Execution:   python generateMCC.py
# Output:      a series of files MCC_(m+a)_a.xyz
################################################################################

import math
from functools import cmp_to_key

# pre-generated list of format [m, a, R]
# MCC of type (m+a)_a with truncation radius R
clusterTypes = [[ 3, 0, 2.61], [ 3, 1,  3.41],
                [ 4, 0, 3.57], [ 4, 1,  4.12], [ 4, 2,  5.01],
                [ 5, 0, 4.29], [ 5, 1,  5.04], [ 5, 2,  6.01],
                [ 6, 0, 5.22], [ 6, 1,  6.01], [ 6, 2,  6.81], [ 6, 3,  7.39],
                [ 7, 0, 5.98], [ 7, 1,  6.70], [ 7, 2,  7.48], [ 7, 3,  8.32],
                [ 8, 0, 6.89], [ 8, 1,  7.63], [ 8, 2,  8.42], [ 8, 3,  9.02], [ 8, 4, 10.00],
                [ 9, 0, 7.82], [ 9, 1,  8.58], [ 9, 2,  9.14], [ 9, 3, 10.01], [ 9, 4, 10.77],
                [10, 0, 8.58], [10, 1,  9.30], [10, 2, 10.07], [10, 3, 11.00], [10, 4, 11.49], [10, 5, 12.31],
                [11, 0, 9.49], [11, 1, 10.23], [11, 2, 11.01], [11, 3, 11.59], [11, 4, 12.40]]

# geometry of the icosahedron
tau = (1.0 + math.sqrt(5.0)) / 2.0
t = tau / math.sqrt(tau + 2.0)
e = 1.0 / math.sqrt(tau + 2.0)
icoVertices = [[ t,  e,  0], [ t, -e,  0], [ e,  0,  t], [ e,  0, -t],
               [ 0,  t,  e], [ 0,  t, -e], [ 0, -t,  e], [ 0, -t, -e],
               [-e,  0,  t], [-e,  0, -t], [-t,  e,  0], [-t, -e,  0]]
icoFaces = [[0,1,2], [0,1,3], [0,2,4], [0,3,5], [0,4,5],
            [1,2,6], [1,3,7], [1,6,7], [2,4,8], [2,6,8],
            [3,5,9], [3,7,9], [4,5,10], [4,8,10], [5,9,10],
            [6,8,11], [6,7,11], [7,9,11], [8,10,11], [9,10,11]]
icoEdges = [[0,1], [0,2], [0,3], [0,4], [0,5], [1,2],
            [1,3], [1,6], [1,7], [2,4], [2,6], [2,8],
            [3,5], [3,7], [3,9], [4,5], [4,8], [4,10],
            [5,9], [5,10], [6,7], [6,8], [6,11], [7,9],
            [7,11], [8,10], [8,11], [9,10], [9,11], [10,11]]

# generate tetrahedron spanned by four vertices
def generateTetrahedron(cluster, m, v0, v1, v2, v3, t):
	# vectors spanning the lattice
	d1 = [v1[0] - v0[0], v1[1] - v0[1], v1[2] - v0[2]]
	d2 = [v2[0] - v0[0], v2[1] - v0[1], v2[2] - v0[2]]
	d3 = [v3[0] - v0[0], v3[1] - v0[1], v3[2] - v0[2]]
	# place all particles
	for i1 in range(0, m + 1):
		for i2 in range(0, m + 1 - i1):
			for i3 in range(0, m + 1 - i1 - i2):
				cluster.add(m * v0[0] + i1 * d1[0] + i2 * d2[0] + i3 * d3[0],
							m * v0[1] + i1 * d1[1] + i2 * d2[1] + i3 * d3[1],
							m * v0[2] + i1 * d1[2] + i2 * d2[2] + i3 * d3[2], t)

# generate MCC
def generateMCC(n):
	cluster = Cluster()
	v0 = [0, 0, 0]

	# part 1: center & faces
	for f in icoFaces:
		# vertices
		v1 = icoVertices[f[0]]
		v2 = icoVertices[f[1]]
		v3 = icoVertices[f[2]]

		# center part
		generateTetrahedron(cluster, n, v0, v1, v2, v3, 0)

		# face part
		v4 = [(v1[0] + v2[0] + v3[0]) * 2.0 / 3.0,
			  (v1[1] + v2[1] + v3[1]) * 2.0 / 3.0,
			  (v1[2] + v2[2] + v3[2]) * 2.0 / 3.0]
		generateTetrahedron(cluster, n, v4, v1, v2, v3, 1)

	# part 2: edges & vertices
	for e in icoEdges:
		# spanning vectors
		v1 = icoVertices[e[0]]
		v2 = icoVertices[e[1]]

		# normal vectors
		s1 = (tau + 1.0) / 3.0
		u1 = [(v1[0] + v2[0]) * s1,
			  (v1[1] + v2[1]) * s1,
			  (v1[2] + v2[2]) * s1]
		s2 = math.sqrt(2.0 + tau) / 3.0
		u2 = [(v1[1] * v2[2] - v1[2] * v2[1]) * s2,
			  (v1[2] * v2[0] - v1[0] * v2[2]) * s2,
			  (v1[0] * v2[1] - v1[1] * v2[0]) * s2]

		# edge part, tetrahedron (1x volume)
		v3 = [u1[0] - u2[0], u1[1] - u2[1], u1[2] - u2[2]]
		v4 = [u1[0] + u2[0], u1[1] + u2[1], u1[2] + u2[2]]
		generateTetrahedron(cluster, n, v1, v2, v3, v4, 2)

		# vertex part, tetrahedron (2x volume)
		v5 = [2 * v1[0], 2 * v1[1], 2 * v1[2]]
		v6 = [2 * v2[0], 2 * v2[1], 2 * v2[2]]
		generateTetrahedron(cluster, n, v1, v5, v3, v4, 3)
		generateTetrahedron(cluster, n, v2, v6, v3, v4, 3)

	return cluster

# class Cluster: handles a set of points that form a cluster
class Cluster:
	def __init__(self):
		self.points = []

	# add to cluster a particle at position [x, y, z] of type t
	def add(self, x, y, z, t):
		self.points.append([x, y, z, t])

	# sort points from the innermost to the outermost and remove duplicates
	def sort(self, R):
		# small number used as a value for numerical 'accuracy'
		EPSILON = 1e-9
		# sort with helper function
		def comparePoints(p1, p2):
			rr1 = p1[0] * p1[0] + p1[1] * p1[1] + p1[2] * p1[2]
			rr2 = p2[0] * p2[0] + p2[1] * p2[1] + p2[2] * p2[2]
			if (rr1 - rr2 > EPSILON): return +2;
			if (rr2 - rr1 > EPSILON): return -2;
			if (p1[0] - p2[0] > EPSILON): return +1;
			if (p2[0] - p1[0] > EPSILON): return -1;
			if (p1[1] - p2[1] > EPSILON): return +1;
			if (p2[1] - p1[1] > EPSILON): return -1;
			if (p1[2] - p2[2] > EPSILON): return +1;
			if (p2[2] - p1[2] > EPSILON): return -1;
			return 0;
		points = sorted(self.points, key = cmp_to_key(comparePoints))

		# remove duplicates and particles outside of radius R
		p0 = [-1, -1, -1]
		points2 = []
		for p in points:
			if (abs(comparePoints(p, p0)) > 0):
				p0 = p
				rr = p[0] * p[0] + p[1] * p[1] + p[2] * p[2]
				if (rr <= R * R):
					points2.append(p)
		return points2

	# output in .xyz format of those particles outside of radius R
	def output(self, file, R):
		points = self.sort(R)
		print(str(len(points)), file = file)
		print('MCC of radius ' + str(R), file = file)
		for p in points:
			if (p[3] == 0): print("H", file = file, end=" ")
			if (p[3] == 1): print("C", file = file, end=" ")
			if (p[3] == 2): print("N", file = file, end=" ")
			if (p[3] == 3): print("O", file = file, end=" ")
			print(p[0], p[1], p[2], file = file)

# generate all cluster types in pre-generated list
for ct in clusterTypes:
	m = ct[0]
	a = ct[1]
	R = ct[2]
	file = open('MCC_' + str(m + a) + '_' + str(a) + '.xyz', 'w')
	cluster = generateMCC(m)
	cluster.output(file, R)
	file.close()
