From 9a6c8eaf912883a2349f38a336b1745360caa51a Mon Sep 17 00:00:00 2001 From: Felipe Lopes Date: Sat, 2 Mar 2024 21:41:20 -0300 Subject: [PATCH] Create pip release --- README.md | 8 +- build/lib/__init__.py | 48 + build/lib/building_block.py | 686 +++ build/lib/cjson.py | 403 ++ build/lib/data/topology.py | 253 + build/lib/exceptions.py | 48 + build/lib/framework.py | 4494 +++++++++++++++++ build/lib/io_tools.py | 1287 +++++ build/lib/logger.py | 87 + build/lib/tools.py | 1006 ++++ dist/pycofbuilder-0.0.7-py3-none-any.whl | Bin 0 -> 43748 bytes dist/pycofbuilder-0.0.7.tar.gz | Bin 0 -> 44246 bytes pyproject.toml | 10 +- requirements.txt | 1 + setup.py | 2 +- .../pycofbuilder.egg-info/PKG-INFO | 205 + .../pycofbuilder.egg-info/SOURCES.txt | 21 + .../dependency_links.txt | 1 + .../pycofbuilder.egg-info/requires.txt | 12 + .../pycofbuilder.egg-info/top_level.txt | 9 + 20 files changed, 8574 insertions(+), 7 deletions(-) create mode 100644 build/lib/__init__.py create mode 100644 build/lib/building_block.py create mode 100644 build/lib/cjson.py create mode 100644 build/lib/data/topology.py create mode 100644 build/lib/exceptions.py create mode 100644 build/lib/framework.py create mode 100644 build/lib/io_tools.py create mode 100644 build/lib/logger.py create mode 100644 build/lib/tools.py create mode 100644 dist/pycofbuilder-0.0.7-py3-none-any.whl create mode 100644 dist/pycofbuilder-0.0.7.tar.gz create mode 100644 src/pycofbuilder/pycofbuilder.egg-info/PKG-INFO create mode 100644 src/pycofbuilder/pycofbuilder.egg-info/SOURCES.txt create mode 100644 src/pycofbuilder/pycofbuilder.egg-info/dependency_links.txt create mode 100644 src/pycofbuilder/pycofbuilder.egg-info/requires.txt create mode 100644 src/pycofbuilder/pycofbuilder.egg-info/top_level.txt diff --git a/README.md b/README.md index feefdc87..9d33f7e9 100644 --- a/README.md +++ b/README.md @@ -28,7 +28,13 @@ conda env create --file environment.yml ## Installation -Currently the best way to use pyCOFBuilder is to manually import it using the `sys` module, as exemplified below: +You can install pyCOFBuilder using pip: + +```Shell +pip install pycofbuilder +``` + +Alternativelly, you can use pyCOFBuilder by manually import it using the `sys` module, as exemplified below: ```python # importing module diff --git a/build/lib/__init__.py b/build/lib/__init__.py new file mode 100644 index 00000000..578f44f8 --- /dev/null +++ b/build/lib/__init__.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Created by Felipe Lopes de Oliveira +# Distributed under the terms of the MIT License. + +import os +import sys + +# This code is written for Python 3 +if sys.version_info[0] != 3: + raise Exception("Sorry but pyCOFBuilder requires Python 3.") + sys.exit(1) + +# Import BuildingBlocks class +from pycofbuilder.building_block import BuildingBlock + +# Import Framework class +from pycofbuilder.framework import Framework + +# Import Tools +import pycofbuilder.tools as Tools +import pycofbuilder.io_tools as IO_Tools + +# Import ChemJSON +import pycofbuilder.chemjson as ChemJSON + +# Import Exceptions +import pycofbuilder.exceptions as Exceptions + +# Import Logger +import pycofbuilder.logger as Logger + +__all__ = [ + 'BuildingBlock', + 'Framework', + 'Tools', + 'IO_Tools', + 'ChemJSON', + 'Exceptions', + 'Logger' + ] + +_ROOT = os.path.abspath(os.path.dirname(__file__)) + +__author__ = "Felipe Lopes de Oliveira" +__license__ = "MIT" +__version__ = '0.0.6' +__email__ = "felipe.lopes@nano.ufrj.br" +__status__ = "Development" diff --git a/build/lib/building_block.py b/build/lib/building_block.py new file mode 100644 index 00000000..0b9c3b74 --- /dev/null +++ b/build/lib/building_block.py @@ -0,0 +1,686 @@ +# -*- coding: utf-8 -*- +# Created by Felipe Lopes de Oliveira +# Distributed under the terms of the MIT License. + +""" +The BuildingBlock class is used to create the building blocks for the Framework class. +""" + +import os +import copy +import numpy as np +from scipy.spatial.transform import Rotation as R + +from pycofbuilder.tools import (rotation_matrix_from_vectors, + closest_atom, + closest_atom_struc, + find_index, + unit_vector) +from pycofbuilder.io_tools import (read_xyz, save_xyz, read_gjf) +from pycofbuilder.cjson import ChemJSON + +from pycofbuilder.logger import create_logger + +from pycofbuilder.exceptions import MissingXError + + +class BuildingBlock(): + + def __init__(self, name: str = '', **kwargs): + + self.name: str = name + + self.out_path: str = kwargs.get('out_dir', os.path.join(os.getcwd(), 'out')) + self.save_bb: bool = kwargs.get('save_bb', True) + self.bb_out_path: str = kwargs.get('bb_out_path', os.path.join(self.out_path, 'building_blocks')) + + self.logger = create_logger(level=kwargs.get('log_level', 'info'), + format=kwargs.get('log_format', 'simple'), + save_to_file=kwargs.get('save_to_file', False), + log_filename=kwargs.get('log_filename', 'pycofbuilder.log')) + + _ROOTDIR = os.path.abspath(os.path.dirname(__file__)) + self.main_path = os.path.join(_ROOTDIR, 'data') + + self.connectivity = kwargs.get('connectivity', None) + self.size = kwargs.get('size', None) + self.mass = kwargs.get('mass', None) + self.composition = kwargs.get('composition', None) + + self.atom_types = kwargs.get('atom_types', None) + self.atom_pos = kwargs.get('atom_pos', None) + self.atom_labels = kwargs.get('atom_labels', None) + + self.smiles = kwargs.get('smiles', None) + self.charge = kwargs.get('charge', 0) + self.multiplicity = kwargs.get('multiplicity', 1) + self.chirality = kwargs.get('chirality', 0) + self.symmetry = kwargs.get('symmetry', None) + + self.core = kwargs.get('core', None) + self.conector = kwargs.get('conector', None) + self.funcGroups = kwargs.get('funcGroups', None) + + self.available_symmetry = ['L2', + 'T3', + 'S4', 'D4', # 'R4' + 'H6', # 'O6', 'P6' + # 'C8', 'A8', 'E8' + # 'B12', 'I12', 'U12', 'X12' + ] + + # Check if bb_out_path exists and try to create it if not + if self.save_bb != '': + os.makedirs(self.bb_out_path, exist_ok=True) + + # If a name is provided create building block from this name + if self.name != '': + if '.' not in self.name: + self.from_name(self.name) + + # If a name is provided and it is a file, create building block from this file + if '.' in self.name: + self.from_file(self.out_path, self.name) + + def __str__(self): + return self.structure_as_string() + + def __repr__(self): + return 'BuildingBlock({}, {}, {}, {})'.format(self.symmetry, + self.core, + self.conector, + self.funcGroups) + + def copy(self): + '''Return a deep copy of the BuildingBlock object''' + return copy.deepcopy(self) + + def from_file(self, path, file_name): + '''Read a building block from a file''' + extension = file_name.split('.')[-1] + + read_func_dict = {'xyz': read_xyz, + 'gjf': read_gjf} + + self.name = file_name.rstrip(f'.{extension}') + self.atom_types, self.atom_pos = read_func_dict[extension](path, file_name) + self.atom_labels = ['C']*len(self.atom_types) + + if any([i == 'X' for i in self.atom_types]): + self.connectivity = len([i for i in self.atom_types if 'X' in i]) + else: + raise MissingXError() + + self.centralize(by_X=True) + self.calculate_size() + pref_orientation = unit_vector( + self.get_X_points()[1][0]) + + self.align_to(pref_orientation) + + def from_name(self, name): + '''Automatically read or create a buiding block based on its name''' + + # Check the existence of the building block files + symm_check, core_check, conector_check, funcGroup_check = self.check_existence(name) + + error_msg = "Building Block name is invalid!\n" + error_msg += "Symm: {}, Core: {}, Connector: {}, Functional Group:{}".format(symm_check, + core_check, + conector_check, + funcGroup_check) + assert all([symm_check, core_check, conector_check, funcGroup_check]), error_msg + + self.name = name + + BB_name = self.name.split('_') + self.symmetry = BB_name[0] + self.core = BB_name[1] + self.conector = BB_name[2] + possible_funcGroups = BB_name[3:] + ['H'] * (9 - len(BB_name[3:])) + + self.create_BB_structure(self.symmetry, + self.core, + self.conector, + *possible_funcGroups) + if self.save_bb: + self.save() + + def n_atoms(self): + ''' Returns the number of atoms in the unitary cell''' + return len(self.atom_types) + + def print_structure(self): + """ + Print the structure in xyz format: + `atom_label pos_x pos_y pos_z` + """ + + print(self.structure_as_string()) + + def centralize(self, by_X=True): + ''' Centralize the molecule on its geometrical center''' + + transposed = np.transpose(self.atom_pos) + if by_X is True: + x_transposed = np.transpose(self.get_X_points()[1]) + if by_X is False: + x_transposed = transposed + cm_x = transposed[0] - np.average(x_transposed[0]) + cm_y = transposed[1] - np.average(x_transposed[1]) + cm_z = transposed[2] - np.average(x_transposed[2]) + + self.atom_pos = np.transpose([cm_x, cm_y, cm_z]) + return np.transpose([cm_x, cm_y, cm_z]) + + def get_X_points(self): + '''Get the X points in a molecule''' + + if 'X' in self.atom_types: + X_labels, X_pos = [], [] + for i in range(len(self.atom_types)): + if self.atom_types[i] == 'X': + X_labels += [self.atom_types[i]] + X_pos += [self.atom_pos[i]] + + return X_labels, np.array(X_pos) + + else: + print('No X ponts could be found!') + return self.atom_types, self.atom_pos + + def get_Q_points(self, atom_types, atom_pos): + '''Get the Q points in a molecule''' + + Q_labels, Q_pos = [], [] + + for i in range(len(atom_types)): + if atom_types[i] == 'Q': + Q_labels += [atom_types[i]] + Q_pos += [atom_pos[i]] + + return Q_labels, np.array(Q_pos) + + def get_R_points(self, atom_types, atom_pos): + """ + Get the R points in a molecule + """ + + # Create a dict with the R points + R_dict = {'R': [], + 'R1': [], + 'R2': [], + 'R3': [], + 'R4': [], + 'R5': [], + 'R6': [], + 'R7': [], + 'R8': [], + 'R9': []} + + # Iterate over the R keys + for key in R_dict.keys(): + # Iterate over the atoms + for i in range(len(atom_types)): + if atom_types[i] == key: + R_dict[key] += [atom_pos[i]] + + return R_dict + + def calculate_size(self): + '''Calculate the size of the building block''' + _, X_pos = self.get_X_points() + self.size = [np.linalg.norm(i) for i in X_pos] + + def align_to(self, vec: list = [0, 1, 0], n: int = 0): + ''' + Align the first n-th X point to a given vector + + Parameters + ---------- + vec : list + Vector to align the molecule + n : int + Index of the X point to be aligned + align_to_y : bool + If True, the second point is aligned to the y axis + ''' + _, X_pos = self.get_X_points() + R_matrix = rotation_matrix_from_vectors(X_pos[n], vec) + + self.atom_pos = np.dot(self.atom_pos, np.transpose(R_matrix)) + + def rotate_around(self, rotation_axis: list = [1, 0, 0], angle: float = 0.0, degree: bool = True): + ''' + Rotate the molecule around a given axis + + Parameters + ---------- + rotation_axis : list + Rotation axis + angle : float + Rotation angle in degrees + degree : bool + If True, the angle is given in degrees. + Default is True. + ''' + + rotation_axis = unit_vector(rotation_axis) + rotation = R.from_rotvec(angle * rotation_axis, degrees=degree) + + self.atom_pos = rotation.apply(self.atom_pos) + + def rotate_to_xy_plane(self): + '''Rotate the molecule to the xy plane''' + _, X_pos = self.get_X_points() + + if len(X_pos) == 3: + + normal = np.cross(X_pos[0], X_pos[-1]) + if normal[0] != 0 and normal[1] != 0: + R_matrix = rotation_matrix_from_vectors(normal, [0, 0, 1]) + self.atom_pos = np.dot(self.atom_pos, np.transpose(R_matrix)) + + if len(X_pos) == 2: + normal = np.cross(X_pos[0], self.atom_pos[1]) + if normal[0] != 0 and normal[1] != 0: + R_matrix = rotation_matrix_from_vectors(normal, [0, 0, 1]) + self.atom_pos = np.dot(self.atom_pos, np.transpose(R_matrix)) + + def shift(self, shift_vector: list): + ''' + Shift the molecule by a given vector + + Parameters + ---------- + shift_vector : list + Shift vector + ''' + + self.atom_pos = np.array(self.atom_pos) + np.array(shift_vector) + + def structure_as_string(self): + struct_string = '' + for i, _ in enumerate(self.atom_types): + struct_string += '{:<5s}{:>10.7f}{:>15.7f}{:>15.7f}\n'.format(self.atom_types[i], + self.atom_pos[i][0], + self.atom_pos[i][1], + self.atom_pos[i][2]) + + return struct_string + + def add_connection_group(self, conector_name): + '''Adds the functional group by which the COF will be formed from the building blocks''' + + connector = ChemJSON() + connector.from_cjson(os.path.join(self.main_path, 'conector'), conector_name) + + self.smiles = self.smiles.replace('[Q]', + f"{connector.properties['smiles'].replace('[Q]', '')}") + + conector_label = connector.atomic_types + conector_pos = connector.cartesian_positions + + # Get the position of the Q points in the structure + location_Q_struct = self.get_Q_points(self.atom_types, self.atom_pos) + + for i in range(len(location_Q_struct[0])): + + n_conector_label = conector_label.copy() + n_conector_pos = conector_pos.copy() + + # Get the position of the closest atom to Q in the structure + close_Q_struct = closest_atom('Q', + location_Q_struct[1][i], + self.atom_types, + self.atom_pos)[1] + + # Get the position of Q in the conection group + location_Q_connector = self.get_Q_points(n_conector_label, n_conector_pos) + + # Get the position of the closest atom to Q in the conection group + close_Q_connector = closest_atom('Q', + location_Q_connector[1][0], + n_conector_label, + n_conector_pos)[1] + + # Create the vector Q in the structure + v1 = close_Q_struct - location_Q_struct[1][i] + # Create the vector Q in the conector + v2 = np.array(close_Q_connector) - np.array(location_Q_connector[1][0]) + + # Find the rotation matrix that align v2 with v1 + Rot_m = rotation_matrix_from_vectors(v2, v1) + + # Delete the "Q" atom position of the conector group and the structure + n_conector_pos = np.delete( + n_conector_pos, + find_index(np.array([0., 0., 0.]), n_conector_pos), + axis=0) + + self.atom_labels = np.delete( + self.atom_labels, + find_index(location_Q_struct[1][i], self.atom_pos), + axis=0 + ) + + self.atom_pos = np.delete( + self.atom_pos, + find_index(location_Q_struct[1][i], self.atom_pos), + axis=0 + ) + + # Rotate and translade the conector group to Q position in the strucutre + rotated_translated_group = np.dot(n_conector_pos, -np.transpose(Rot_m)) + location_Q_struct[1][i] + + # Add the position of conector atoms to the main structure + self.atom_pos = np.append(self.atom_pos, rotated_translated_group, axis=0) + + # Remove the Q atoms from structure + self.atom_types.remove('Q') + n_conector_label.remove('Q') + + self.atom_types = self.atom_types + n_conector_label + + self.atom_labels = np.append(self.atom_labels, [['Q'] * len(n_conector_label)]) + + def add_connection_group_symm(self, conector_name): + '''Adds the functional group by which the COF will be formed from the building blocks''' + + connector = ChemJSON() + connector.from_cjson(os.path.join(self.main_path, 'conector'), conector_name) + + self.smiles = self.smiles.replace('[Q]', + f"{connector.properties['smiles'].replace('[Q]', '')}") + + conector_types = connector.atomic_types + conector_pos = connector.cartesian_positions + + # Get the position of the Q points in the structure + _, Q_vec = self.get_Q_points(self.atom_types, self.atom_pos) + + # Remove the Q atoms from structure + self.atom_types = self.atom_types[:-4] + self.atom_pos = self.atom_pos[:-4] + self.atom_labels = self.atom_labels[:-4] + + # Create the vector Q in the structure + QS_vector = Q_vec[0] + + # Create the vector Q in the conector + QC_vector = np.array(conector_pos[1]) + + # Find the rotation matrix that align the connector with the structure + Rot_m = rotation_matrix_from_vectors(QC_vector, QS_vector) + + # Rotate and translade the conector group to Q position in the strucutre + conector_pos = np.dot(conector_pos, np.transpose(Rot_m)) + Q_vec[0] + conector_pos = R.from_rotvec( + -90 * unit_vector(conector_pos[1] - conector_pos[0]), degrees=True).apply(conector_pos) + + # Add the position of conector atoms to the main structure + self.atom_types = list(self.atom_types) + conector_types[1:] + self.atom_pos = list(self.atom_pos) + list(conector_pos[1:]) + self.atom_labels = np.append(self.atom_labels, [['Q'] * len(conector_types[1:])]) + + # Apply the improper rotations to match S4 symmetry + Q_vec = [unit_vector(i) for i in Q_vec] + + # First S4 axis is location_Q_struct[1] + R1 = R.from_rotvec(120 * Q_vec[1], degrees=True).apply(conector_pos) + R1 = R.from_rotvec(60 * unit_vector(R1[1] - R1[0]), degrees=True).apply(R1) + + # Add the position of conector atoms to the main structure + self.atom_types = list(self.atom_types) + conector_types[1:] + self.atom_pos = list(self.atom_pos) + list(R1[1:]) + self.atom_labels = np.append(self.atom_labels, [['Q'] * len(conector_types[1:])]) + + # Second S4 axis is location_Q_struct[2] + R2 = R.from_rotvec(120 * Q_vec[2], degrees=True).apply(conector_pos) + R2 = R.from_rotvec(-120 * unit_vector(R2[1]-R2[0]), degrees=True).apply(R2) + + # Add the position of conector atoms to the main structure + self.atom_types = list(self.atom_types) + conector_types[1:] + self.atom_pos = list(self.atom_pos) + list(R2)[1:] + self.atom_labels = np.append(self.atom_labels, [['Q'] * len(conector_types[1:])]) + + # Third S4 axis is location_Q_struct[3] + R3 = R.from_rotvec(120 * Q_vec[3], degrees=True).apply(conector_pos) + R3 = R.from_rotvec(60 * unit_vector(R3[1] - R3[0]), degrees=True).apply(R3) + + # Add the position of conector atoms to the main structure + self.atom_types = list(self.atom_types) + conector_types[1:] + self.atom_pos = list(self.atom_pos) + list(R3[1:]) + self.atom_labels = np.append(self.atom_labels, [['Q'] * len(conector_types[1:])]) + + def add_R_group(self, R_name, R_type): + '''Adds group R in building blocks''' + + rgroup = ChemJSON() + rgroup.from_cjson(os.path.join(self.main_path, 'func_groups'), R_name) + + self.smiles = self.smiles.replace(f'[{R_type}]', + f"{rgroup.properties['smiles'].replace('[Q]', '')}") + + group_label = rgroup.atomic_types + group_pos = rgroup.cartesian_positions + + # Get the position of the R points in the structure + location_R_struct = self.get_R_points(self.atom_types, self.atom_pos)[R_type] + + # Get the position of the R points in the R group + for i, _ in enumerate(location_R_struct): + n_group_label = group_label.copy() + n_group_pos = group_pos.copy() + + # Get the position of the closest atom to R in the structure + close_R_struct = closest_atom_struc(R_type, + location_R_struct[i], + self.atom_types, + self.atom_pos)[1] + + # Get the position of R in the R group + pos_R_group = self.get_R_points(n_group_label, n_group_pos)['R'] + + # Get the position of the closest atom to R in the R group + close_R_group = closest_atom('R', pos_R_group[0], n_group_label, n_group_pos)[1] + + # Create the vector R in the structure + v1 = close_R_struct - location_R_struct[i] + + # Create the vector R in the R group + v2 = np.array(close_R_group) - np.array(pos_R_group[0]) + + # Find the rotation matrix that align v2 with v1 + Rot_m = rotation_matrix_from_vectors(v2, v1) + + # Delete the "R" atom position of the R group and the structure + n_group_pos = np.delete( + n_group_pos, + find_index(np.array([0.0, 0.0, 0.0]), n_group_pos), + axis=0 + ) + + # Rotate and translade the R group to R position in the strucutre + rotated_translated_group = np.dot( + n_group_pos, + -np.transpose(Rot_m) + ) + location_R_struct[i] + + # Remove the R atoms from the labels list + # Remove the R atoms from structure + self.atom_labels = np.delete( + self.atom_labels, + find_index(location_R_struct[i], self.atom_pos), + axis=0 + ) + + # Remove the R atoms from structure + self.atom_pos = np.delete( + self.atom_pos, + find_index(location_R_struct[i], self.atom_pos), + axis=0 + ) + + # Add the position of rotated atoms to the main structure + self.atom_pos = np.append(self.atom_pos, rotated_translated_group, axis=0) + + # Remove the R atoms from structure + self.atom_types.remove(R_type) + + # Remove the R atoms from R group + n_group_label.remove('R') + + self.atom_types = self.atom_types + n_group_label + self.atom_labels = np.append(self.atom_labels, ['R'] * len(n_group_label)) + + def create_BB_structure(self, + symmetry='L2', + core_name='BENZ', + conector='CHO', + R1='H', + R2='H', + R3='H', + R4='H', + R5='H', + R6='H', + R7='H', + R8='H', + R9='H'): + '''Create a building block''' + + self.name = f'{symmetry}_{core_name}_{conector}' + + core = ChemJSON() + core.from_cjson(os.path.join(self.main_path, 'core', symmetry), core_name) + + self.smiles = core.properties['smiles'] + + self.atom_types = core.atomic_types + self.atom_pos = core.cartesian_positions + self.composition = core.formula + self.atom_labels = ['C']*len(self.atom_types) + + pref_orientation = unit_vector( + self.get_Q_points(core.atomic_types, core.cartesian_positions)[1][0]) + + if symmetry == 'D4': + self.add_connection_group_symm(conector) + else: + self.add_connection_group(conector) + + R_list_names = [R1, R2, R3, R4, R5, R6, R7, R8, R9] + R_list_labels = ['R1', 'R2', 'R3', 'R4', 'R5', 'R6', 'R7', 'R8', 'R9'] + + funcGroup_string = [] + for i in range(len(R_list_names)): + if R_list_labels[i] in self.atom_types: + self.add_R_group(R_list_names[i], R_list_labels[i]) + self.name += f'_{R_list_names[i]}' + funcGroup_string.append(R_list_names[i]) + + self.funcGroups = funcGroup_string + + self.connectivity = len([i for i in self.atom_types if 'X' in i]) + self.centralize() + self.align_to(pref_orientation) + self.calculate_size() + + def replace_X(self, target_type): + for i in range(len(self.atom_types)): + if self.atom_types[i] == "X": + self.atom_types[i] = target_type + + def remove_X(self): + + atom_types, atom_pos, atom_labels = [], [], [] + for i in range(len(self.atom_types)): + if self.atom_types[i] != "X": + atom_types.append(self.atom_types[i]) + atom_pos.append(self.atom_pos[i]) + atom_labels.append(self.atom_labels[i]) + + self.atom_types = atom_types + self.atom_pos = np.array(atom_pos) + self.atom_labels = atom_labels + + def save(self, extension='xyz'): + + if extension == 'xyz': + save_xyz(path=self.bb_out_path, + file_name=self.name + '.xyz', + atom_types=self.atom_types, + atom_pos=self.atom_pos) + + def get_available_core(self): + '''Get the list of available cores''' + + available_cores = {} + + for symm in self.available_symmetry: + symm_path = os.path.join(self.main_path, 'core', symm) + available_cores[symm] = [i.rstrip('.cjson') for i in os.listdir(symm_path) if '.cjson' in i] + + return available_cores + + def get_available_R(self): + '''Get the list of available functional groups''' + R_PATH = os.path.join(self.main_path, 'func_groups') + R_list = [i.rstrip('.cjson') for i in os.listdir(R_PATH) if '.cjson' in i] + + return R_list + + def get_available_conector(self): + '''Get the list of available conectores''' + C_PATH = os.path.join(self.main_path, 'conector') + C_list = [i.rstrip('.cjson') for i in os.listdir(C_PATH) if '.cjson' in i] + + return C_list + + def check_existence(self, name): + + symm_check = False + core_check = False + conector_check = False + funcGroup_check = True + + name = name.split('_') + symm = name[0] + core = name[1] + conector = name[2] + funcGroups = name[3:] + + BB_dict = self.get_available_core() + + if symm in self.available_symmetry: + symm_check = True + else: + print('ERROR!: Building Block symmetry must be L2, T3, S4, or H6.') + symm_check = False + + if core in BB_dict[symm]: + core_check = True + else: + print(f'ERROR!: {core} not available!') + print(f'Available cores with {symm} symmetry are {BB_dict[symm]}') + + if conector in self.get_available_conector(): + conector_check = True + else: + print(f'ERROR! {conector} is not a available conector.') + print(f'Available list: {self.get_available_conector()}') + + possible_funcGroups_list = self.get_available_R() + for func in funcGroups: + if func not in possible_funcGroups_list: + print(f'ERROR! Functional group {func} is not a available.') + print(f'Available list: {possible_funcGroups_list}') + funcGroup_check = False + + return symm_check, core_check, conector_check, funcGroup_check + + def get_buildingblock_list(self, shape, connector_group): + + files_list = os.listdir(self.bb_out_path) + + return [i.rstrip('.xyz') for i in files_list if shape == i.split('_')[0] and connector_group in i.split('_')[2]] diff --git a/build/lib/cjson.py b/build/lib/cjson.py new file mode 100644 index 00000000..f980126d --- /dev/null +++ b/build/lib/cjson.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +# Created by Felipe Lopes de Oliveira +# Distributed under the terms of the MIT License. + +""" +The CJSON package implements functions to read, create and manipulate Chemical JSON objects. +""" + +import os +import simplejson +import numpy as np + +from pycofbuilder.tools import elements_dict + +import gemmi +from ase.cell import Cell + + +class ChemJSON: + ''' + Class to read, create and manupulate ChemJSON files. + + Attributes + ---------- + + file_name : str + The name of the file. + name : str + The name of the structure. + cell_parameters : list + The cell parameters of the structure as a (1,6) list. + cell_matrix : list + The cell matrix of the structure as a (3,3) list. + cartesian_positions : list + The cartesian positions of the structure as a (n,3) list. + fractional_positions : list + The fractional positions of the structure as a (n,3) list. + atomic_numbers : list + The atomic numbers of the structure as a (n,1) list. + atomic_types : list + The atomic types of the structure as a (n,1) list. + atomic_labels : list + The atomic labels of the structure as a (n,1) list. + formula : str + The formula of the structure. + properties : dict + The properties of the structure. + partial_charges : dict + A dictionary contaning the partial charges of the atoms on the structure. + Example: {'DDEC': [0.1, 0.2, 0.15], 'EQeq': [0.05, 0.15, 0.19]} + ''' + def __init__(self): + self.file_name = '' + self.name = '' + + # Structure properties + self.cell_parameters = None + self.cell_matrix = None + self.cartesian_positions = None + self.fractional_positions = None + self.atomic_numbers = None + self.atomic_types = None + self.atomic_labels = None + self.formula = '' + self.partial_charges = None + + self.properties = None + self.results = [] + + # Create a custom representation of the class + def __repr__(self): + ''' + Returns a custom representation of the class. + ''' + + repr_string = "ChemJSON(name='{}', formula='{}', number of atoms={}".format(self.name, + self.formula, + len(self.atomic_types)) + + return repr_string + + # Create a custom print of the class + def __str__(self): + ''' + Returns a custom print of the class. + ''' + + string_string = "ChemJSON(name='{}', formula='{}', number of atoms={})\n".format(self.name, + self.formula, + len(self.atomic_types)) + + if self.cell_parameters is not None: + string_string += f"""Cell parameters: + a = {self.cell_parameters[0]:>12.7f} Å + b = {self.cell_parameters[1]:>12.7f} Å + c = {self.cell_parameters[2]:>12.7f} Å + α = {self.cell_parameters[3]:>12.7f} ° + β = {self.cell_parameters[4]:>12.7f} ° + γ = {self.cell_parameters[5]:>12.7f} ° + +Cell matrix: +A {self.cell_matrix[0][0]:>12.7f} {self.cell_matrix[0][1]:>12.7f} {self.cell_matrix[0][2]:>12.7f} +B {self.cell_matrix[1][0]:>12.7f} {self.cell_matrix[1][1]:>12.7f} {self.cell_matrix[1][2]:>12.7f} +C {self.cell_matrix[2][0]:>12.7f} {self.cell_matrix[2][1]:>12.7f} {self.cell_matrix[2][2]:>12.7f} +""" + if self.cartesian_positions is not None: + string_string += "Cartesian positions:\n" + for i, position in enumerate(self.cartesian_positions): + string_string += " {:3} {:>9.5f} {:>9.5f} {:>9.5f}\n".format(self.atomic_types[i], + position[0], + position[1], + position[2] + ) + + if self.fractional_positions is not None: + string_string += "Fractional positions:\n" + for i, position in enumerate(self.fractional_positions): + string_string += " {:3} {:>9.5f} {:>9.5f} {:>9.5f}\n".format(self.atomic_types[i], + position[0], + position[1], + position[2]) + + return string_string + + def set_properties(self, properties): + ''' + Sets the properties of the structure. + ''' + self.properties = properties + + def set_results(self, results): + ''' + Sets the results of the structure. + ''' + self.results = results + + def set_cell_parameters(self, cell_parameters): + ''' + Sets the cell parameters of the structure. + ''' + self.cell_parameters = cell_parameters + + aseCell = Cell.fromcellpar(cell_parameters) + self.cell_matrix = np.array(aseCell) + + def set_cell_matrix(self, cell_matrix): + ''' + Sets the cell matrix of the structure. The cell + parameters will be calculated and also updated. + ''' + self.cell_matrix = cell_matrix + + aseCell = Cell(cell_matrix) + self.cell_parameters = aseCell.cellpar() + + def set_cartesian_positions(self, cartesian_positions): + ''' + Sets the cartesian positions of the structure. The fractional + positions will be calculated and also updated. + ''' + self.cartesian_positions = np.array(cartesian_positions).astype(float) + + if self.cell_parameters is not None: + aseCell = Cell.fromcellpar(self.cell_parameters) + self.fractional_positions = aseCell.scaled_positions(cartesian_positions) + + def set_fractional_positions(self, fractional_positions): + ''' + Sets the fractional positions of the structure. The cartesian + positions will be calculated and also updated. + ''' + self.fractional_positions = np.array(fractional_positions).astype(float) + + if self.cell_parameters is not None: + aseCell = Cell.fromcellpar(self.cell_parameters) + self.cartesian_positions = aseCell.cartesian_positions(fractional_positions) + + def set_atomic_types(self, atomic_types): + ''' + Sets the atomic labels of the structure. + ''' + self.atomic_types = atomic_types + + self.atomic_labels = [f"{atom}{i+1}" for i, atom in enumerate(self.atomic_types)] + + symbol_dict = elements_dict('atomic_number') + self.atomic_numbers = [symbol_dict[i] for i in atomic_types] + + self.formula = ''.join([f'{atom}{self.atomic_types.count(atom)}' for atom in set(self.atomic_types)]) + + def set_atomic_numbers(self, atomic_numbers): + ''' + Sets the atomic numbers of the structure. The atomic types and formula + will be calculated and also updated. + ''' + self.atomic_numbers = atomic_numbers + + symbol_dict = elements_dict('atomic_number') + number_dict = {j: i for i, j in zip(symbol_dict.keys(), symbol_dict.values())} + + self.atomic_types = [number_dict[i] for i in atomic_numbers] + + self.atomic_labels = [f"{atom}{i+1}" for i, atom in enumerate(self.atomic_types)] + + self.formula = ''.join([f'{atom}{self.atomic_types.count(atom)}' for atom in set(self.atomic_types)]) + + def from_cjson(self, path, file_name): + ''' + Reads a ChemJSON file from a given path and file_name. + ''' + self.file_name = os.path.join(path, file_name.split('.')[0] + '.cjson') + + with open(self.file_name, 'r') as file: + cjson_data = simplejson.load(file) + + if "name" in cjson_data: + self.name = cjson_data['name'] + + if "unitCell" in cjson_data: + if 'a' in cjson_data['unitCell']: + self.set_cell_parameters( + [cjson_data['unitCell'][i] for i in ['a', 'b', 'c', 'alpha', 'beta', 'gamma']] + ) + elif 'cellVectors' in cjson_data['unitCell']: + self.set_cell_matrix( + np.array(cjson_data['unitCell']['cellVectors']).reshape(3, 3) + ) + + if "atoms" in cjson_data: + if 'coords' in cjson_data['atoms']: + if '3d' in cjson_data['atoms']['coords']: + self.set_cartesian_positions( + np.array(cjson_data['atoms']['coords']['3d']).reshape(-1, 3) + ) + elif '3dFractional' in cjson_data['atoms']['coords']: + self.set_fractional_positions( + np.array(cjson_data['atoms']['coords']['3dFractional']).reshape(-1, 3) + ) + + if "elements" in cjson_data['atoms']: + if 'type' in cjson_data['atoms']['elements']: + self.set_atomic_types(cjson_data['atoms']['elements']['type']) + + elif 'number' in cjson_data['atoms']['elements']: + self.set_atomic_numbers(cjson_data['atoms']['elements']['number']) + + if 'label' in cjson_data['atoms']['elements']: + self.atomic_labels = cjson_data['atoms']['elements']['label'] + else: + self.atomic_labels = [f"{atom}{i+1}" for i, atom in enumerate(self.atomic_types)] + + if 'properties' in cjson_data: + self.set_properties(cjson_data['properties']) + if 'results' in cjson_data: + self.set_results(cjson_data['results']) + if 'partialCharges' in cjson_data: + self.partial_charges = cjson_data['partialCharges'] + + def from_xyz(self, path, file_name): + ''' + Reads a XYZ file from a given path and file_name. + ''' + + self.file_name = os.path.join(path, file_name.split('.')[0] + '.xyz') + self.name = file_name.split('.')[0] + + with open(self.file_name, 'r') as file: + xyz_data = file.read().splitlines() + n_atoms = int(xyz_data[0]) + + atomic_types = [] + cartesian_positions = [] + + for line in xyz_data[2: n_atoms + 3]: + atomic_types.append(line.split()[0]) + cartesian_positions.append([float(i) for i in line.split()[1:]]) + + self.set_atomic_types(atomic_types) + + self.set_cartesian_positions(np.array(cartesian_positions)) + + def from_gjf(self, path, file_name): + ''' + Reads a Gaussian input file from a given path and file_name. + ''' + + self.file_name = os.path.join(path, file_name.split('.')[0] + '.gjf') + self.name = file_name.split('.')[0] + + with open(self.file_name, 'r') as file: + gjf_data = file.read().splitlines() + + # Remove empty lines + gjf_data = [line for line in gjf_data if line != ''] + + atomic_types = [] + cartesian_positions = [] + + for line in gjf_data: + if line.split()[0] in elements_dict('atomic_number').keys(): + atomic_types.append(line.split()[0]) + cartesian_positions.append([float(i) for i in line.split()[1:4]]) + + cell_matrix = [] + for line in gjf_data: + if line.split()[0] == 'Tv': + cell_matrix.append([float(i) for i in line.split()[1:4]]) + + if cell_matrix != []: + self.set_cell_matrix(np.array(cell_matrix)) + + self.set_atomic_types(atomic_types) + + self.set_cartesian_positions(np.array(cartesian_positions)) + + def from_cif(self, path, file_name): + ''' + Reads a CIF file from a given path and file_name. + ''' + + # Read the cif file and get the lattice parameters and atomic positions + cif_filename = os.path.join(path, file_name.split('.')[0] + '.cif') + + cif = gemmi.cif.read_file(cif_filename).sole_block() + + a = float(cif.find_value('_cell_length_a').split('(')[0]) + b = float(cif.find_value('_cell_length_b').split('(')[0]) + c = float(cif.find_value('_cell_length_c').split('(')[0]) + beta = float(cif.find_value('_cell_angle_beta').split('(')[0]) + gamma = float(cif.find_value('_cell_angle_gamma').split('(')[0]) + alpha = float(cif.find_value('_cell_angle_alpha').split('(')[0]) + + CellParameters = [a, b, c, alpha, beta, gamma] + + AtomicTypes = list(cif.find_values('_atom_site_type_symbol')) + PosX = np.array(cif.find_values('_atom_site_fract_x')).astype(float) + PosY = np.array(cif.find_values('_atom_site_fract_y')).astype(float) + PosZ = np.array(cif.find_values('_atom_site_fract_z')).astype(float) + try: + charges = np.array(cif.find_values('_atom_site_charge')).astype(float) + charge_type = 'DDEC' + except Exception: + charges = None + charge_type = None + + self.set_cell_parameters(CellParameters) + + self.set_atomic_types(AtomicTypes) + + self.set_fractional_positions(np.array([PosX, PosY, PosZ]).T) + + if charges is not None: + self.partial_charges = {charge_type: charges} + + def as_dict(self): + ''' + Returns the structure as a dictionary. + ''' + structure_dict = { + 'chemical json': 1, + 'name': self.name, + 'formula': self.formula, + } + if self.cell_parameters is not None: + structure_dict['unit cell'] = { + 'a': self.cell_parameters[0], + 'b': self.cell_parameters[1], + 'c': self.cell_parameters[2], + 'alpha': self.cell_parameters[3], + 'beta': self.cell_parameters[4], + 'gamma': self.cell_parameters[5], + 'cellVectors': self.cell_matrix.flatten().tolist() + } + + structure_dict['atoms'] = { + 'elements': { + 'type': self.atomic_types, + 'number': self.atomic_numbers, + }, + 'coords': { + '3d': self.cartesian_positions.flatten().tolist(), + } + } + + if self.cell_parameters is not None: + structure_dict['atoms']['coords']['3dFractional'] = self.fractional_positions.flatten().tolist() + + if self.partial_charges is not None: + structure_dict['partialCharges'] = self.partial_charges + + structure_dict['properties'] = self.properties + + structure_dict['results'] = self.results + + return structure_dict + + def write_cjson(self, path, file_name): + ''' + Writes a ChemJSON file to a given path and file_name. + ''' + self.file_name = os.path.join(path, file_name.split('.')[0] + '.cjson') + with open(self.file_name, 'w') as file: + simplejson.dump(self.as_dict(), file, indent=4) diff --git a/build/lib/data/topology.py b/build/lib/data/topology.py new file mode 100644 index 00000000..2351f00f --- /dev/null +++ b/build/lib/data/topology.py @@ -0,0 +1,253 @@ +# -*- coding: utf-8 -*- +# Created by Felipe Lopes de Oliveira +# Distributed under the terms of the MIT License. + +""" +The dictionary containing the definitions nets for a Framework buiding +""" + +import numpy as np + +TOPOLOGY_DICT = { + 'HCB': { + 'a': 2*np.cos(np.radians(30)), + 'b': 2*np.cos(np.radians(30)), + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 120, + 'vertice_connectivity': 3, + 'edge_connectivity': 0, + 'vertices': [ + {'position': [0, 0, 0], 'angle': 0}, + {'position': [0, np.sqrt(3)/3, 0], 'angle': 180} + ], + 'edges': [] + }, + 'HCB_A': { + 'a': 2*np.cos(np.radians(30))*2, + 'b': 2*np.cos(np.radians(30))*2, + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 120, + 'vertice_connectivity': 3, + 'edge_connectivity': 3, + 'vertices': [ + {'position': [0, 0, 0], 'angle': 0}, + {'position': [0, np.sqrt(3)/3, 0], 'angle': 180} + ], + 'edges': [ + {'position': [0, np.sqrt(3)/6, 0], 'angle': 0}, + {'position': [-1/4, 5*np.sqrt(3)/12, 0], 'angle': 120}, + {'position': [1/4, 5*np.sqrt(3)/12, 0], 'angle': 240} + ] + }, + 'SQL': { + 'a': 2/np.sqrt(2), + 'b': 2/np.sqrt(2), + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 90, + 'vertice_connectivity': 4, + 'edge_connectivity': 0, + 'vertices': [ + {'position': [0, 0, 0], 'angle': 0}, + {'position': [1/2, 1/2, 0], 'angle': 0} + ], + 'edges': [] + }, + 'SQL_A': { + 'a': 4/np.sqrt(2), + 'b': 4/np.sqrt(2), + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 90, + 'vertice_connectivity': 4, + 'edge_connectivity': 2, + 'vertices': [ + {'position': [0, 0, 0], 'angle': 0}, + {'position': [1/2, 1/2, 0], 'angle': 0} + ], + 'edges': [ + {'position': [1/4, 1/4, 0], 'angle': 45}, + {'position': [3/4, 1/4, 0], 'angle': 135}, + {'position': [3/4, 3/4, 0], 'angle': 225}, + {'position': [1/4, 3/4, 0], 'angle': 315}, + ] + }, + 'KGD': { + 'a': 2*np.cos(np.radians(30)), + 'b': 2*np.cos(np.radians(30)), + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 120, + 'vertice_connectivity': 6, + 'edge_connectivity': 3, + 'vertices': [ + {'position': [0, 0, 0], 'angle': 0}, + ], + 'edges': [ + {'position': [0, np.sqrt(3)/3, 0], 'angle': -180}, + {'position': [0.5, np.sqrt(3)/6, 0], 'angle': 0} + ] + }, + 'HXL_A': { + 'a': 2, + 'b': 2, + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 120, + 'vertice_connectivity': 6, + 'edge_connectivity': 0, + 'vertices': [ + {'position': [0, 0, 0], 'angle': 30}, + ], + 'edges': [ + {'position': [1/4, np.sqrt(3)/4, 0], 'angle': 30}, + {'position': [0.5, 0, 0], 'angle': 90}, + {'position': [-1/4, np.sqrt(3)/4, 0], 'angle': -30} + ] + }, + 'KGM': { + 'a': 4, + 'b': 4, + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 120, + 'vertice_connectivity': 4, + 'edge_connectivity': 0, + 'vertices': [ + {'position': [1/4, np.sqrt(3)/4, 0], 'angle': 30}, + {'position': [1/2, 0, 0], 'angle': -90}, + {'position': [-1/4, np.sqrt(3)/4, 0], 'angle': -30} + ], + 'edges': [] + }, + 'KGM_A': { + 'a': 4, + 'b': 4, + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 120, + 'vertice_connectivity': 4, + 'edge_connectivity': 2, + 'vertices': [ + {'position': [1/4, np.sqrt(3)/4, 0], 'angle': 30}, + {'position': [1/2, 0, 0], 'angle': -90}, + {'position': [-1/4, np.sqrt(3)/4, 0], 'angle': -30} + ], + 'edges': [ + {'position': [3/8, np.sqrt(3)/8, 0], 'angle': -30}, + {'position': [1/8, 3*np.sqrt(3)/8, 0], 'angle': -30}, + {'position': [5/8, np.sqrt(3)/8, 0], 'angle': 30}, + {'position': [-1/8, np.sqrt(3)/8, 0], 'angle': 30}, + {'position': [4/8, np.sqrt(3)/4, 0], 'angle': 90}, + {'position': [0, np.sqrt(3)/4, 0], 'angle': 90}, + ] + }, + 'FXT': { + 'a': 1, + 'b': 1, + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 120, + 'vertice_connectivity': 4, + 'edge_connectivity': 2, + 'vertices': [ + {'position': [1/4, 3*np.sqrt(3)/12, 0], 'angle': -15}, + {'position': [0.5, 0, 0], 'angle': 45}, + {'position': [-1/4, 3*np.sqrt(3)/12, 0], 'angle': 15} + ], + 'edges': [] + }, + 'FXT_A': { + 'a': 2, + 'b': 2, + 'c': 3.6, + 'alpha': 90, + 'beta': 90, + 'gamma': 120, + 'vertice_connectivity': 4, + 'edge_connectivity': 2, + 'vertices': [ + {'position': [1/4, 3*np.sqrt(3)/12, 0], 'angle': -15}, + {'position': [0.5, 0, 0], 'angle': 45}, + {'position': [-1/4, 3*np.sqrt(3)/12, 0], 'angle': 15} + ], + 'edges': [ + {'position': [22/64, 7*np.sqrt(3)/64, 0], 'angle': -30}, + {'position': [85/128, 7*np.sqrt(3)/64, 0], 'angle': 30}, + {'position': [4/8, 35*np.sqrt(3)/128, 0], 'angle': 90}, + {'position': [0, 29*np.sqrt(3)/128, 0], 'angle': 90}, + {'position': [21/128, 25*np.sqrt(3)/64, 0], 'angle': -30}, + {'position': [-21/128, 25*np.sqrt(3)/64, 0], 'angle': 30}, + ] + }, + 'DIA': { + 'a': 1, + 'b': 1, + 'c': 1, + 'alpha': 60, + 'beta': 60, + 'gamma': 60, + 'lattice': [[0, 1, 1], [1, 0, 1], [1, 1, 0]], + 'vertice_connectivity': 4, + 'edge_connectivity': 4, + 'vertices': [ + {'position': [0, 0, 0], 'angle': 55, 'align_v': [1, 1, 1]}, + {'position': [1/4, 1/4, 1/4], 'angle': -55, 'align_v': [-1, -1, -1]}, + ], + 'edges': [] + }, + 'DIA_A': { + 'a': 1, + 'b': 1, + 'c': 1, + 'alpha': 60, + 'beta': 60, + 'gamma': 60, + 'lattice': [[0, 1, 1], [1, 0, 1], [1, 1, 0]], + 'vertice_connectivity': 4, + 'edge_connectivity': 2, + 'vertices': [ + {'position': [0, 0, 0], 'angle': -7.5, 'align_v': [1, 1, 1]}, + {'position': [1/4, 1/4, 1/4], 'angle': 7.5, 'align_v': [-1, -1, -1]}, + ], + 'edges': [ + {'position': [1/8, 1/8, 1/8], 'angle': -8.8, 'align_v': [1, 1, 1]}, + {'position': [1/8, 3/8, 3/8], 'angle': 16, 'align_v': [-1/4, 1/4, 1/4]}, + {'position': [3/8, 1/8, 3/8], 'angle': -78, 'align_v': [1/4, -1/4, 1/4]}, + {'position': [3/8, 3/8, 1/8], 'angle': 16, 'align_v': [1/4, 1/4, -1/4]}, + ] + }, + 'BOR': { + 'a': 1, + 'b': 1, + 'c': 1, + 'alpha': 90, + 'beta': 90, + 'gamma': 90, + 'lattice': [[1, 0, 0], [0, 1, 0], [0, 0, 1]], + 'vertice_connectivity': 4, + 'edge_connectivity': 3, + 'vertices': [ + {'position': [1/2, 0, 0], 'angle': -10, 'align_v': [-1, 1, 1]}, + {'position': [0, 1/2, 0], 'angle': 40, 'align_v': [1, -1, 1]}, + {'position': [0, 0, 1/2], 'angle': -45, 'align_v': [1, 1, -1]}, + ], + 'edges': [ + {'position': [1/6, 1/6, 1/6], 'angle': 30, 'align_v': [1, 1, 1]}, + {'position': [1/6, 5/6, 5/6], 'angle': 0, 'align_v': [1/2, 1, 1]}, + {'position': [5/6, 1/6, 5/6], 'angle': 0, 'align_v': [1, 1/2, 1]}, + {'position': [5/6, 5/6, 1/6], 'angle': 10, 'align_v': [1, 1, 1]}, + ] + } + } diff --git a/build/lib/exceptions.py b/build/lib/exceptions.py new file mode 100644 index 00000000..613aac5c --- /dev/null +++ b/build/lib/exceptions.py @@ -0,0 +1,48 @@ +# -*- coding: utf-8 -*- +# Created by Felipe Lopes de Oliveira +# Distributed under the terms of the MIT License. + +""" +This file implements custom exceptions for the pycofbuilder package. +""" + + +class BondLenghError(Exception): + """Exception raised when the bond length is shorter than 0.8 angstrom.""" + def __init__(self, atom_1=None, atom_2=None, dist=0, threshold=0.8): + self.message = 'WARNING: Atoms {} and {} are closer than {} A, {}'.format(atom_1, + atom_2, + dist, + threshold) + + def __str__(self): + return str(self.message) + + +class BBConnectivityError(Exception): + """Exception raised when the building block connectivity is not valid.""" + def __init__(self, connectivity=None, found_connectivity=None): + self.message = 'ERROR: The building block connectivity should be {} buy is {}'.format(connectivity, + found_connectivity) + + def __str__(self): + return str(self.message) + + +class ConnectionGroupError(Exception): + """Exception raised when the connection group is not valid.""" + def __init__(self, conn_1=None, conn_2=None): + self.message = 'ERROR: The connection group {} not compatible with {}'.format(conn_1, + conn_2) + + def __str__(self): + return str(self.message) + + +class MissingXError(Exception): + """Exception raised when the building block has missing X atoms.""" + def __init__(self): + self.message = 'No X points found in the structure!' + + def __str__(self): + return str(self.message) diff --git a/build/lib/framework.py b/build/lib/framework.py new file mode 100644 index 00000000..20376e51 --- /dev/null +++ b/build/lib/framework.py @@ -0,0 +1,4494 @@ +# -*- coding: utf-8 -*- +# Created by Felipe Lopes de Oliveira +# Distributed under the terms of the MIT License. + +""" +The Framework class implements definitions and methods for a Framework buiding +""" + +import os +import copy +import numpy as np + +# Import pymatgen +from pymatgen.core import Lattice, Structure +from pymatgen.symmetry.analyzer import SpacegroupAnalyzer + +from scipy.spatial.transform import Rotation as R + +# Import pycofbuilder exceptions +from pycofbuilder.exceptions import (BondLenghError, + BBConnectivityError) + +# Import pycofbuilder building_block +from pycofbuilder.building_block import BuildingBlock + +# Import pycofbuilder topology data +from pycofbuilder.data.topology import TOPOLOGY_DICT + +# Import pycofbuilder tools +from pycofbuilder.tools import (get_bond_atom, + cell_to_cellpar, + cellpar_to_cell, + rotation_matrix_from_vectors, + unit_vector, + angle, + get_framework_symm_text) + +# Import pycofbuilder io_tools +from pycofbuilder.io_tools import (save_json, + save_chemjson, + save_cif, + save_xyz, + save_turbomole, + save_vasp, + save_xsf, + save_pdb, + save_pqr, + save_qe, + save_gjf) + +from pycofbuilder.logger import create_logger + + +class Framework(): + """ + A class used to represent a Covalent Organic Framework as a reticular entity. + + ... + + Attributes + ---------- + name : str + Name of the material + out_dir : str + Path to save the results. + If not defined, a `out` folder will be created in the current directory. + verbosity : str + Control the printing options. Can be 'none', 'normal', or 'debug'. + Default: 'normal' + save_bb : bool + Control the saving of the building blocks. + Default: True + lib_path : str + Path for saving the building block files. + If not defined, a `building_blocks` folder will be created in the current directory. + topology : str = None + dimention : str = None + lattice : str = None + lattice_sgs : str = None + space_group : str = None + space_group_n : str = None + stacking : str = None + mass : str = None + composition : str = None + charge : int = 0 + multiplicity : int = 1 + chirality : bool = False + atom_types : list = [] + atom_pos : list = [] + lattice : list = [[], [], []] + symm_tol : float = 0.2 + angle_tol : float = 0.2 + dist_threshold : float = 0.8 + available_2D_topologies : list + List of available 2D topologies + available_3D_topologies : list + List of available 3D topologies + available_topologies : list + List of all available topologies + available_stacking : list + List of available stakings for all 2D topologies + lib_bb : str + String with the name of the folder containing the building block files + Default: bb_lib + """ + + def __init__(self, name: str = None, **kwargs): + + self.name: str = name + + self.out_path: str = kwargs.get('out_dir', os.path.join(os.getcwd(), 'out')) + self.save_bb: bool = kwargs.get('save_bb', True) + self.bb_out_path: str = kwargs.get('bb_out_path', os.path.join(self.out_path, 'building_blocks')) + + self.logger = create_logger(level=kwargs.get('log_level', 'info'), + format=kwargs.get('log_format', 'simple'), + save_to_file=kwargs.get('save_to_file', False), + log_filename=kwargs.get('log_filename', 'pycofbuilder.log')) + + self.symm_tol = kwargs.get('symm_tol', 0.1) + self.angle_tol = kwargs.get('angle_tol', 0.5) + self.dist_threshold = kwargs.get('dist_threshold', 0.8) + + self.bb1_name = None + self.bb2_name = None + self.topology = None + self.stacking = None + self.smiles = None + + self.atom_types = [] + self.atom_pos = [] + self.atom_labels = [] + self.cellMatrix = np.eye(3) + self.cellParameters = np.array([1, 1, 1, 90, 90, 90]).astype(float) + + self.lattice_sgs = None + self.space_group = None + self.space_group_n = None + + self.dimention = None + self.n_atoms = self.get_n_atoms() + self.mass = None + self.composition = None + self.charge = 0 + self.multiplicity = 1 + self.chirality = False + + self.available_2D_top = ['HCB', 'HCB_A', + 'SQL', 'SQL_A', + 'KGD', + 'HXL', 'HXL_A', + 'FXT', 'FXT_A'] + + # To add: ['dia', 'bor', 'srs', 'pts', 'ctn', 'rra', 'fcc', 'lon', 'stp', 'acs', 'tbo', 'bcu', 'fjh', 'ceq'] + self.available_3D_top = ['DIA', 'DIA_A', 'BOR'] # Temporary + self.available_topologies = self.available_2D_top + self.available_3D_top + + # Define available stackings for all 2D topologies + self.available_stacking = { + 'HCB': ['A', 'AA', 'AB1', 'AB2', 'AAl', 'AAt', 'ABC1', 'ABC2'], + 'HCB_A': ['A', 'AA', 'AB1', 'AB2', 'AAl', 'AAt', 'ABC1', 'ABC2'], + 'SQL': ['A', 'AA', 'AB1', 'AB2', 'AAl', 'AAt', 'ABC1', 'ABC2'], + 'SQL_A': ['A', 'AA', 'AB1', 'AB2', 'AAl', 'AAt', 'ABC1', 'ABC2'], + 'KGD': ['A', 'AA', 'AB1', 'AB2', 'AAl', 'AAt', 'ABC1', 'ABC2'], + 'HXL_A': ['A', 'AA', 'AB1', 'AB2', 'AAl', 'AAt', 'ABC1', 'ABC2'], + 'FXT': ['A', 'AA', 'AB1', 'AB2', 'AAl', 'AAt', 'ABC1', 'ABC2'], + 'FXT_A': ['A', 'AA', 'AB1', 'AB2', 'AAl', 'AAt', 'ABC1', 'ABC2'], + 'DIA': [str(i + 1) for i in range(15)], + 'DIA_A': [str(i + 1) for i in range(15)], + 'BOR': [str(i + 1) for i in range(15)] + } + + if self.name is not None: + self.from_name(self.name) + + def __str__(self) -> str: + return self.as_string() + + def __repr__(self) -> str: + return f'Framework({self.bb1_name}, {self.bb2_name}, {self.topology}, {self.stacking})' + + def as_string(self) -> str: + """ + Returns a string with the Framework information. + """ + + fram_str = f'Name: {self.name}\n' + + # Get the formula of the framework + + if self.composition is not None: + fram_str += f'Full Formula ({self.composition})\n' + fram_str += f'Reduced Formula: ({self.composition})\n' + else: + fram_str += 'Full Formula ()\n' + fram_str += 'Reduced Formula: \n' + + fram_str += 'abc : {:11.6f} {:11.6f} {:11.6f}\n'.format(*self.cellParameters[:3]) + fram_str += 'angles: {:11.6f} {:11.6f} {:11.6f}\n'.format(*self.cellParameters[3:]) + fram_str += 'A: {:11.6f} {:11.6f} {:11.6f}\n'.format(*self.cellMatrix[0]) + fram_str += 'B: {:11.6f} {:11.6f} {:11.6f}\n'.format(*self.cellMatrix[1]) + fram_str += 'C: {:11.6f} {:11.6f} {:11.6f}\n'.format(*self.cellMatrix[2]) + + fram_str += f'Cartesian Sites ({self.n_atoms})\n' + fram_str += ' # Type a b c label\n' + fram_str += '--- ---- -------- -------- -------- -------\n' + + for i in range(len(self.atom_types)): + fram_str += '{:3d} {:4s} {:8.5f} {:8.5f} {:8.5f} {:>7}\n'.format(i, + self.atom_types[i], + self.atom_pos[i][0], + self.atom_pos[i][1], + self.atom_pos[i][2], + self.atom_labels[i]) + + return fram_str + + def get_n_atoms(self) -> int: + ''' Returns the number of atoms in the unitary cell''' + return len(self.atom_types) + + def get_available_topologies(self, dimensionality: str = 'all', print_result: bool = True): + """ + Get the available topologies implemented in the class. + + Parameters + ---------- + + dimensionality : str, optional + The dimensionality of the topologies to be printed. Can be 'all', '2D' or '3D'. + Default: 'all' + print_result: bool, optional + If True, the available topologies are printed. + + Returns + ------- + dimensionality_list: list + A list with the available topologies. + """ + + dimensionality_error = f'Dimensionality must be one of the following: all, 2D, 3D, not {dimensionality}' + assert dimensionality in ['all', '2D', '3D'], dimensionality_error + + dimensionality_list = [] + + if dimensionality == 'all' or dimensionality == '2D': + if print_result: + print('Available 2D Topologies:') + for i in self.available_2D_top: + if print_result: + print(i.upper()) + dimensionality_list.append(i) + + if dimensionality == 'all' or dimensionality == '3D': + if print_result: + print('Available 3D Topologies:') + for i in self.available_3D_top: + if print_result: + print(i.upper()) + dimensionality_list.append(i) + + return dimensionality_list + + def check_name_concistency(self, FrameworkName) -> tuple[str, str, str, str]: + """ + Checks if the name is in the correct format and returns a + tuple with the building blocks names, the net and the stacking. + + In case the name is not in the correct format, an error is raised. + + Parameters + ---------- + FrameworkName : str, required + The name of the COF to be created + + Returns + ------- + tuple[str, str, str, str] + A tuple with the building blocks names, the net and the stacking. + """ + + string_error = 'FrameworkName must be in the format: BB1_BB2_Net_Stacking' + assert isinstance(FrameworkName, str), string_error + + name_error = 'FrameworkName must be in the format: BB1_BB2_Net_Stacking' + assert len(FrameworkName.split('-')) == 4, name_error + + bb1_name, bb2_name, Net, Stacking = FrameworkName.split('-') + + net_error = f'{Net} not in the available list: {self.available_topologies}' + assert Net in self.available_topologies, net_error + + stacking_error = f'{Stacking} not in the available list: {self.available_stacking[Net]}' + assert Stacking in self.available_stacking[Net], stacking_error + + return bb1_name, bb2_name, Net, Stacking + + def from_name(self, FrameworkName, **kwargs) -> None: + """Creates a COF from a given FrameworkName. + + Parameters + ---------- + FrameworkName : str, required + The name of the COF to be created + + Returns + ------- + COF : Framework + The COF object + """ + bb1_name, bb2_name, Net, Stacking = self.check_name_concistency(FrameworkName) + + bb1 = BuildingBlock(name=bb1_name, bb_out_path=self.bb_out_path, save_bb=self.save_bb) + bb2 = BuildingBlock(name=bb2_name, bb_out_path=self.bb_out_path, save_bb=self.save_bb) + + self.from_building_blocks(bb1, bb2, Net, Stacking, **kwargs) + + def from_building_blocks(self, + bb1: BuildingBlock, + bb2: BuildingBlock, + net: str, + stacking: str, + **kwargs): + """Creates a COF from the building blocks. + + Parameters + ---------- + BB1 : BuildingBlock, required + The first building block + BB2 : BuildingBlock, required + The second building block + Net : str, required + The network of the COF + Stacking : str, required + The stacking of the COF + + Returns + ------- + COF : Framework + The COF object + """ + self.name = f'{bb1.name}-{bb2.name}-{net}-{stacking}' + self.bb1_name = bb1.name + self.bb2_name = bb2.name + self.topology = net + self.stacking = stacking + + # Check if the BB1 has the smiles attribute + if hasattr(bb1, 'smiles') and hasattr(bb2, 'smiles'): + self.smiles = f'{bb1.smiles}.{bb2.smiles}' + else: + print('WARNING: The smiles attribute is not available for the building blocks') + + net_build_dict = { + 'HCB': self.create_hcb_structure, + 'HCB_A': self.create_hcb_a_structure, + 'SQL': self.create_sql_structure, + 'SQL_A': self.create_sql_a_structure, + 'KGD': self.create_kgd_structure, + # 'HXL': self.create_hxl_structure, + 'HXL_A': self.create_hxl_a_structure, + 'FXT': self.create_fxt_structure, + 'FXT_A': self.create_fxt_a_structure, + 'DIA': self.create_dia_structure, + 'DIA_A': self.create_dia_a_structure, + 'BOR': self.create_bor_structure + } + + result = net_build_dict[net](bb1, bb2, stacking, **kwargs) + + return result + + def save(self, fmt: str = 'cif', supercell: list = [1, 1, 1], save_dir=None, primitive=False) -> None: + ''' + Save the structure in a specif file format. + + Parameters + ---------- + fmt : str, optional + The file format to be saved + Can be `json`, `cif`, `xyz`, `turbomole`, `vasp`, `xsf`, `pdb`, `pqr`, `qe`. + Default: 'cif' + supercell : list, optional + The supercell to be used to save the structure. + Default: [1,1,1] + save_dir : str, optional + The path to save the structure. By default, the structure is saved in a + `out` folder created in the current directory. + primitive : bool, optional + If True, the primitive cell is saved. Otherwise, the conventional cell is saved. + Default: False + ''' + + save_dict = { + 'json': save_json, + 'cjson': save_chemjson, + 'cif': save_cif, + 'xyz': save_xyz, + 'turbomole': save_turbomole, + 'vasp': save_vasp, + 'xsf': save_xsf, + 'pdb': save_pdb, + 'pqr': save_pqr, + 'qe': save_qe, + 'gjf': save_gjf + } + + file_format_error = f'Format must be one of the following: {save_dict.keys()}' + assert fmt in save_dict.keys(), file_format_error + + if primitive: + structure = self.prim_structure + + else: + structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + final_structure = structure.make_supercell(supercell, in_place=False) + + structure_dict = final_structure.as_dict() + + cell = structure_dict['lattice']['matrix'] + + atom_types = [site['species'][0]['element'] for site in structure_dict['sites']] + atom_labels = [site['properties']['source'] for site in structure_dict['sites']] + atom_pos = [site['xyz'] for site in structure_dict['sites']] + + if save_dir is None: + save_path = self.out_path + else: + save_path = save_dir + + save_dict[fmt](path=save_path, + file_name=self.name, + cell=cell, + atom_types=atom_types, + atom_labels=atom_labels, + atom_pos=atom_pos) + +# --------------- Net creation methods -------------------------- # + + def create_hcb_structure(self, + BB_T3_A, + BB_T3_B, + stacking: str = 'AA', + slab: float = 10.0, + shift_vector: list = [1.0, 1.0, 0], + tilt_angle: float = 5.0): + """Creates a COF with HCB network. + + The HCB net is composed of two tripodal building blocks. + + Parameters + ---------- + BB_T3_1 : BuildingBlock, required + The BuildingBlock object of the tripodal Buiding Block A + BB_T3_2 : BuildingBlock, required + The BuildingBlock object of the tripodal Buiding Block B + stacking : str, optional + The stacking pattern of the COF layers (default is 'AA') + slab : float, optional + Default parameter for the interlayer slab (default is 10.0) + shift_vector: list, optional + Shift vector for the AAl and AAt stakings (defatult is [1.0,1.0,0]) + tilt_angle: float, optional + Tilt angle for the AAt staking in degrees (default is 5.0) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + + connectivity_error = 'Building block {} must present connectivity {} not {}' + + if BB_T3_A.connectivity != 3: + self.logger.error(connectivity_error.format('A', 3, BB_T3_A.connectivity)) + raise BBConnectivityError(3, BB_T3_A.connectivity) + if BB_T3_B.connectivity != 3: + self.logger.error(connectivity_error.format('B', 3, BB_T3_B.connectivity)) + raise BBConnectivityError(3, BB_T3_B.connectivity) + + self.name = f'{BB_T3_A.name}-{BB_T3_B.name}-HCB-{stacking}' + self.topology = 'HCB' + self.staking = stacking + self.dimension = 2 + + self.charge = BB_T3_A.charge + BB_T3_B.charge + self.chirality = BB_T3_A.chirality or BB_T3_B.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_T3_A.conector, BB_T3_B.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_T3_A.conector, + BB_T3_B.conector)) + + # Replace "X" the building block + BB_T3_A.replace_X(bond_atom) + + # Remove the "X" atoms from the the building block + BB_T3_A.remove_X() + BB_T3_B.remove_X() + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = BB_T3_A.size[0] + BB_T3_B.size[0] + + # Calculate the delta size to add to the c parameter + delta_a = abs(max(np.transpose(BB_T3_A.atom_pos)[2])) + abs(min(np.transpose(BB_T3_A.atom_pos)[2])) + delta_b = abs(max(np.transpose(BB_T3_B.atom_pos)[2])) + abs(min(np.transpose(BB_T3_B.atom_pos)[2])) + + delta_max = max([delta_a, delta_b]) + + # Calculate the cell parameters + a = topology_info['a'] * size + b = topology_info['b'] * size + c = topology_info['c'] + delta_max + alpha = topology_info['alpha'] + beta = topology_info['beta'] + gamma = topology_info['gamma'] + + if self.stacking == 'A': + c = slab + + # Create the lattice + self.cellMatrix = Lattice.from_parameters(a, b, c, alpha, beta, gamma) + self.cellParameters = np.array([a, b, c, alpha, beta, gamma]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Add the A1 building blocks to the structure + vertice_data = topology_info['vertices'][0] + self.atom_types += BB_T3_A.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', + vertice_data['angle'], + degrees=True).as_matrix() + + rotated_pos = np.dot(BB_T3_A.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C1' if i == 'C' else i for i in BB_T3_A.atom_labels] + + # Add the A2 building block to the structure + vertice_data = topology_info['vertices'][1] + self.atom_types += BB_T3_B.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', + vertice_data['angle'], + degrees=True).as_matrix() + + rotated_pos = np.dot(BB_T3_B.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C2' if i == 'C' else i for i in BB_T3_B.atom_labels] + + # Creates a pymatgen structure + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + # Translates the structure to the center of the cell + StartingFramework.translate_sites( + range(len(StartingFramework.as_dict()['sites'])), + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + self.cellParameters = cell_to_cellpar(self.cellMatrix) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + + if stacking == 'A' or stacking == 'AA': + stacked_structure = StartingFramework + + if stacking == 'AB1': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [2/3, 1/3, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AB2': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/2, 0, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/2, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC1': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (2/3, 1/3, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (4/3, 2/3, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC2': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 0, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 0, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AAl': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + sv = np.array(shift_vector) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos + sv)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + # Create AA tilted stacking. + if stacking == 'AAt': + cell = StartingFramework.as_dict()['lattice'] + + # Shift the cell by the tilt angle + a_cell = cell['a'] + b_cell = cell['b'] + c_cell = cell['c'] * 2 + alpha = cell['alpha'] - tilt_angle + beta = cell['beta'] - tilt_angle + gamma = cell['gamma'] + + self.cellMatrix = cellpar_to_cell([a_cell, b_cell, c_cell, alpha, beta, gamma]) + self.cellParameters = np.array([a_cell, b_cell, c_cell, alpha, beta, gamma]).astype(float) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = stacked_structure.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + self.cellParameters = cell_to_cellpar(self.cellMatrix) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = stacked_structure.formula + + dist_matrix = StartingFramework.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(stacked_structure, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_refined_structure(keep_site_properties=True) + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_hcb_a_structure(self, + BB_T3: str, + BB_L2: str, + stacking: str = 'AA', + slab: float = 10.0, + shift_vector: list = [1.0, 1.0, 0], + tilt_angle: float = 5.0): + """Creates a COF with HCB-A network. + + The HCB-A net is composed of one tripodal and one linear building blocks. + + Parameters + ---------- + BB_T3 : BuildingBlock, required + The BuildingBlock object of the tripodal Buiding Block + BB_L2 : BuildingBlock, required + The BuildingBlock object of the linear Buiding Block + stacking : str, optional + The stacking pattern of the COF layers (default is 'AA') + c_parameter_base : float, optional + The base value for interlayer distance in angstroms (default is 3.6) + print_result : bool, optional + Parameter for the control for printing the result (default is True) + slab : float, optional + Default parameter for the interlayer slab (default is 10.0) + shift_vector: list, optional + Shift vector for the AAl and AAt stakings (defatult is [1.0,1.0,0]) + tilt_angle: float, optional + Tilt angle for the AAt staking in degrees (default is 5.0) + + Returns + ------- + list + A list of strings containing: + 1. the structure name + 2. lattice type + 3. hall symbol of the cristaline structure + 4. space group + 5. number of the space group, + 6. number of operation symmetry + """ + + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_T3.connectivity != 3: + self.logger.error(connectivity_error.format('A', 3, BB_T3.connectivity)) + raise BBConnectivityError(3, BB_T3.connectivity) + if BB_L2.connectivity != 2: + self.logger.error(connectivity_error.format('B', 3, BB_L2.connectivity)) + raise BBConnectivityError(2, BB_L2.connectivity) + + self.name = f'{BB_T3.name}-{BB_L2.name}-HCB_A-{stacking}' + self.topology = 'HCB_A' + self.staking = stacking + self.dimension = 2 + + self.charge = BB_L2.charge + BB_T3.charge + self.chirality = BB_L2.chirality or BB_T3.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_T3.conector, BB_L2.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_T3.conector, + BB_L2.conector)) + + # Replace "X" the building block + BB_L2.replace_X(bond_atom) + + # Remove the "X" atoms from the the building block + BB_T3.remove_X() + BB_L2.remove_X() + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = BB_T3.size[0] + BB_L2.size[0] + + # Calculate the delta size to add to the c parameter + delta_a = abs(max(np.transpose(BB_T3.atom_pos)[2])) + abs(min(np.transpose(BB_T3.atom_pos)[2])) + delta_b = abs(max(np.transpose(BB_L2.atom_pos)[2])) + abs(min(np.transpose(BB_L2.atom_pos)[2])) + + delta_max = max([delta_a, delta_b]) + + # Calculate the cell parameters + a = topology_info['a'] * size + b = topology_info['b'] * size + c = topology_info['c'] + delta_max + alpha = topology_info['alpha'] + beta = topology_info['beta'] + gamma = topology_info['gamma'] + + if self.stacking == 'A': + c = slab + + # Create the lattice + self.cellMatrix = Lattice.from_parameters(a, b, c, alpha, beta, gamma) + self.cellParameters = np.array([a, b, c, alpha, beta, gamma]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Add the building blocks to the structure + for vertice_data in topology_info['vertices']: + self.atom_types += BB_T3.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', vertice_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_T3.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C1' if i == 'C' else i for i in BB_T3.atom_labels] + + # Add the building blocks to the structure + for edge_data in topology_info['edges']: + self.atom_types += BB_L2.atom_types + + R_Matrix = R.from_euler('z', edge_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_L2.atom_pos, R_Matrix) + np.array(edge_data['position'])*a + + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C2' if i == 'C' else i for i in BB_L2.atom_labels] + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + # Translates the structure to the center of the cell + StartingFramework.translate_sites( + range(len(StartingFramework.as_dict()['sites'])), + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + + if stacking == 'A' or stacking == 'AA': + stacked_structure = StartingFramework + + if stacking == 'AB1': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [2/3, 1/3, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AB2': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/2, 0, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/2, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC1': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (2/3, 1/3, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (4/3, 2/3, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC2': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 0, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 0, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AAl': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + sv = np.array(shift_vector) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos + sv)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + # Create AA tilted stacking. + if stacking == 'AAt': + cell = StartingFramework.as_dict()['lattice'] + + # Shift the cell by the tilt angle + a_cell = cell['a'] + b_cell = cell['b'] + c_cell = cell['c'] * 2 + alpha = cell['alpha'] - tilt_angle + beta = cell['beta'] - tilt_angle + gamma = cell['gamma'] + + self.cellMatrix = cellpar_to_cell([a_cell, b_cell, c_cell, alpha, beta, gamma]) + self.cellParameters = np.array([a_cell, b_cell, c_cell, alpha, beta, gamma]).astype(float) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = stacked_structure.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = stacked_structure.formula + + dist_matrix = stacked_structure.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(stacked_structure, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_refined_structure(keep_site_properties=True) + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_sql_structure(self, + BB_S4_A: str, + BB_S4_B: str, + stacking: str = 'AA', + slab: float = 10.0, + shift_vector: list = [1.0, 1.0, 0], + tilt_angle: float = 5.0): + """Creates a COF with SQL network. + + The SQL net is composed of two tetrapodal building blocks. + + Parameters + ---------- + BB_S4_A : BuildingBlock, required + The BuildingBlock object of the tetrapodal Buiding Block A + BB_S4_B : BuildingBlock, required + The BuildingBlock object of the tetrapodal Buiding Block B + stacking : str, optional + The stacking pattern of the COF layers (default is 'AA') + print_result : bool, optional + Parameter for the control for printing the result (default is True) + slab : float, optional + Default parameter for the interlayer slab (default is 10.0) + shift_vector: list, optional + Shift vector for the AAl and AAt stakings (defatult is [1.0,1.0,0]) + tilt_angle: float, optional + Tilt angle for the AAt staking in degrees (default is 5.0) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_S4_A.connectivity != 4: + self.logger.error(connectivity_error.format('A', 4, BB_S4_A.connectivity)) + raise BBConnectivityError(4, BB_S4_A.connectivity) + if BB_S4_B.connectivity != 4: + self.logger.error(connectivity_error.format('B', 4, BB_S4_B.connectivity)) + raise BBConnectivityError(4, BB_S4_B.connectivity) + + self.name = f'{BB_S4_A.name}-{BB_S4_B.name}-SQL-{stacking}' + self.topology = 'SQL' + self.staking = stacking + self.dimension = 2 + + self.charge = BB_S4_A.charge + BB_S4_B.charge + self.chirality = BB_S4_A.chirality or BB_S4_B.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_S4_A.conector, BB_S4_B.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_S4_A.conector, + BB_S4_B.conector)) + + # Replace "X" the building block + BB_S4_A.replace_X(bond_atom) + + # Remove the "X" atoms from the the building block + BB_S4_A.remove_X() + BB_S4_B.remove_X() + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = BB_S4_A.size[0] + BB_S4_B.size[0] + + # Calculate the delta size to add to the c parameter + delta_a = abs(max(np.transpose(BB_S4_A.atom_pos)[2])) + abs(min(np.transpose(BB_S4_B.atom_pos)[2])) + delta_b = abs(max(np.transpose(BB_S4_A.atom_pos)[2])) + abs(min(np.transpose(BB_S4_B.atom_pos)[2])) + + delta_max = max([delta_a, delta_b]) + + # Calculate the cell parameters + a = topology_info['a'] * size + b = topology_info['b'] * size + c = topology_info['c'] + delta_max + alpha = topology_info['alpha'] + beta = topology_info['beta'] + gamma = topology_info['gamma'] + + if self.stacking == 'A': + c = slab + + # Create the lattice + self.cellMatrix = Lattice.from_parameters(a, b, c, alpha, beta, gamma) + self.cellParameters = np.array([a, b, c, alpha, beta, gamma]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Add the first building block to the structure + vertice_data = topology_info['vertices'][0] + self.atom_types += BB_S4_A.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', vertice_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_S4_A.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C1' if i == 'C' else i for i in BB_S4_A.atom_labels] + + # Add the second building block to the structure + vertice_data = topology_info['vertices'][1] + self.atom_types += BB_S4_B.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', vertice_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_S4_B.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C2' if i == 'C' else i for i in BB_S4_B.atom_labels] + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + # Translates the structure to the center of the cell + StartingFramework.translate_sites( + range(len(StartingFramework.as_dict()['sites'])), + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + + if stacking == 'A' or stacking == 'AA': + stacked_structure = StartingFramework + + if stacking == 'AB1': + + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/4, 1/4, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/4, 1/4, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AB2': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/2, 0, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/2, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC1': + + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (1/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 1/3, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 2/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 2/3, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC2': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (1/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 0, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 2/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 0, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + # Create AAl stacking. Tetragonal cell with two sheets + # per cell shifited by the shift_vector in angstroms. + if stacking == 'AAl': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + sv = np.array(shift_vector) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos + sv)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + # Create AA tilted stacking. + # Tilted tetragonal cell with two sheets per cell tilted by tilt_angle. + if stacking == 'AAt': + cell = StartingFramework.as_dict()['lattice'] + + # Shift the cell by the tilt angle + a_cell = cell['a'] + b_cell = cell['b'] + c_cell = cell['c'] * 2 + alpha = cell['alpha'] - tilt_angle + beta = cell['beta'] - tilt_angle + gamma = cell['gamma'] + + self.cellMatrix = cellpar_to_cell([a_cell, b_cell, c_cell, alpha, beta, gamma]) + self.cellParameters = np.array([a_cell, b_cell, c_cell, alpha, beta, gamma]).astype(float) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = stacked_structure.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = stacked_structure.formula + + dist_matrix = stacked_structure.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(stacked_structure, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_refined_structure(keep_site_properties=True) + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_sql_a_structure(self, + BB_S4: str, + BB_L2: str, + stacking: str = 'AA', + c_parameter_base: float = 3.6, + slab: float = 10.0, + shift_vector: list = [1.0, 1.0, 0], + tilt_angle: float = 5.0): + """Creates a COF with SQL-A network. + + The SQL-A net is composed of one tetrapodal and one linear building blocks. + + Parameters + ---------- + BB_S4 : BuildingBlock, required + The BuildingBlock object of the tetrapodal Buiding Block + BB_L2 : BuildingBlock, required + The BuildingBlock object of the bipodal Buiding Block + stacking : str, optional + The stacking pattern of the COF layers (default is 'AA') + slab : float, optional + Default parameter for the interlayer slab (default is 10.0) + shift_vector: list, optional + Shift vector for the AAl and AAt stakings (defatult is [1.0,1.0,0]) + tilt_angle: float, optional + Tilt angle for the AAt staking in degrees (default is 5.0) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_S4.connectivity != 4: + self.logger.error(connectivity_error.format('A', 4, BB_S4.connectivity)) + raise BBConnectivityError(4, BB_S4.connectivity) + if BB_L2.connectivity != 2: + self.logger.error(connectivity_error.format('B', 3, BB_L2.connectivity)) + raise BBConnectivityError(2, BB_L2.connectivity) + + self.name = f'{BB_S4.name}-{BB_L2.name}-SQL_A-{stacking}' + self.topology = 'SQL_A' + self.staking = stacking + self.dimension = 2 + + self.charge = BB_S4.charge + BB_L2.charge + self.chirality = BB_S4.chirality or BB_L2.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_S4.conector, BB_L2.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_S4.conector, + BB_L2.conector)) + + # Replace "X" the building block + BB_L2.replace_X(bond_atom) + + # Remove the "X" atoms from the the building block + BB_S4.remove_X() + BB_L2.remove_X() + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = BB_S4.size[0] + BB_L2.size[0] + + # Calculate the delta size to add to the c parameter + delta_a = abs(max(np.transpose(BB_S4.atom_pos)[2])) + abs(min(np.transpose(BB_S4.atom_pos)[2])) + delta_b = abs(max(np.transpose(BB_L2.atom_pos)[2])) + abs(min(np.transpose(BB_L2.atom_pos)[2])) + + delta_max = max([delta_a, delta_b]) + + # Calculate the cell parameters + a = topology_info['a'] * size + b = topology_info['b'] * size + c = topology_info['c'] + delta_max + alpha = topology_info['alpha'] + beta = topology_info['beta'] + gamma = topology_info['gamma'] + + if self.stacking == 'A': + c = slab + + # Create the lattice + self.cellMatrix = Lattice.from_parameters(a, b, c, alpha, beta, gamma) + self.cellParameters = np.array([a, b, c, alpha, beta, gamma]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Add the building blocks to the structure + for vertice_data in topology_info['vertices']: + self.atom_types += BB_S4.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', vertice_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_S4.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C1' if i == 'C' else i for i in BB_S4.atom_labels] + + # Add the building blocks to the structure + for edge_data in topology_info['edges']: + self.atom_types += BB_L2.atom_types + edge_pos = np.array(edge_data['position'])*a + + R_Matrix = R.from_euler('z', edge_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_L2.atom_pos, R_Matrix) + edge_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C2' if i == 'C' else i for i in BB_L2.atom_labels] + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + # Translates the structure to the center of the cell + StartingFramework.translate_sites( + range(len(StartingFramework.as_dict()['sites'])), + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + + if stacking == 'A' or stacking == 'AA': + stacked_structure = StartingFramework + + if stacking == 'AB1': + + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/4, 1/4, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/4, 1/4, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AB2': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/2, 0, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/2, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC1': + + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (1/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 1/3, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 2/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 2/3, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC2': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (1/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 0, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 2/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 0, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + # Create AAl stacking. Tetragonal cell with two sheets + # per cell shifited by the shift_vector in angstroms. + if stacking == 'AAl': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + sv = np.array(shift_vector) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos + sv)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + # Create AA tilted stacking. + # Tilted tetragonal cell with two sheets per cell tilted by tilt_angle. + if stacking == 'AAt': + cell = StartingFramework.as_dict()['lattice'] + + # Shift the cell by the tilt angle + a_cell = cell['a'] + b_cell = cell['b'] + c_cell = cell['c'] * 2 + alpha = cell['alpha'] - tilt_angle + beta = cell['beta'] - tilt_angle + gamma = cell['gamma'] + + self.cellMatrix = cellpar_to_cell([a_cell, b_cell, c_cell, alpha, beta, gamma]) + self.cellParameters = np.array([a_cell, b_cell, c_cell, alpha, beta, gamma]).astype(float) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = stacked_structure.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = stacked_structure.formula + + dist_matrix = stacked_structure.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(stacked_structure, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_refined_structure(keep_site_properties=True) + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_kgd_structure(self, + BB_H6: str, + BB_T3: str, + stacking: str = 'AA', + print_result: bool = True, + slab: float = 10.0, + shift_vector: list = [1.0, 1.0, 0], + tilt_angle: float = 5.0): + """Creates a COF with KGD network. + + The KGD net is composed of one hexapodal and one tripodal building blocks. + + Parameters + ---------- + BB_H6 : BuildingBlock, required + The BuildingBlock object of the hexapodal Buiding Block + BB_T3 : BuildingBlock, required + The BuildingBlock object of the tripodal Buiding Block + stacking : str, optional + The stacking pattern of the COF layers (default is 'AA') + c_parameter_base : float, optional + The base value for interlayer distance in angstroms (default is 3.6) + print_result : bool, optional + Parameter for the control for printing the result (default is True) + slab : float, optional + Default parameter for the interlayer slab (default is 10.0) + shift_vector: list, optional + Shift vector for the AAl and AAt stakings (defatult is [1.0,1.0,0]) + tilt_angle: float, optional + Tilt angle for the AAt staking in degrees (default is 5.0) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_H6.connectivity != 6: + self.logger.error(connectivity_error.format('A', 6, BB_H6.connectivity)) + raise BBConnectivityError(6, BB_H6.connectivity) + if BB_T3.connectivity != 3: + self.logger.error(connectivity_error.format('B', 3, BB_T3.connectivity)) + raise BBConnectivityError(3, BB_T3.connectivity) + + self.name = f'{BB_H6.name}-{BB_T3.name}-KGD-{stacking}' + self.topology = 'KGD' + self.staking = stacking + self.dimension = 2 + + self.charge = BB_H6.charge + BB_T3.charge + self.chirality = BB_H6.chirality or BB_T3.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_H6.conector, BB_T3.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_H6.conector, + BB_T3.conector)) + + # Replace "X" the building block + BB_H6.replace_X(bond_atom) + + # Remove the "X" atoms from the the building block + BB_H6.remove_X() + BB_T3.remove_X() + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = BB_H6.size[0] + BB_T3.size[0] + + # Calculate the delta size to add to the c parameter + delta_a = abs(max(np.transpose(BB_H6.atom_pos)[2])) + abs(min(np.transpose(BB_H6.atom_pos)[2])) + delta_b = abs(max(np.transpose(BB_T3.atom_pos)[2])) + abs(min(np.transpose(BB_T3.atom_pos)[2])) + + delta_max = max([delta_a, delta_b]) + + # Calculate the cell parameters + a = topology_info['a'] * size + b = topology_info['b'] * size + c = topology_info['c'] + delta_max + alpha = topology_info['alpha'] + beta = topology_info['beta'] + gamma = topology_info['gamma'] + + if self.stacking == 'A': + c = slab + + # Create the lattice + self.cellMatrix = Lattice.from_parameters(a, b, c, alpha, beta, gamma) + self.cellParameters = np.array([a, b, c, alpha, beta, gamma]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Add the building blocks to the structure + for vertice_data in topology_info['vertices']: + self.atom_types += BB_H6.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', + vertice_data['angle'], + degrees=True).as_matrix() + + rotated_pos = np.dot(BB_H6.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C1' if i == 'C' else i for i in BB_H6.atom_labels] + + # Add the building blocks to the structure + for edge_data in topology_info['edges']: + self.atom_types += BB_T3.atom_types + edge_pos = np.array(edge_data['position'])*a + + R_Matrix = R.from_euler('z', + edge_data['angle'], + degrees=True).as_matrix() + + rotated_pos = np.dot(BB_T3.atom_pos, R_Matrix) + edge_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C2' if i == 'C' else i for i in BB_T3.atom_labels] + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + # Translates the structure to the center of the cell + StartingFramework.translate_sites( + range(len(StartingFramework.as_dict()['sites'])), + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + + if stacking == 'A' or stacking == 'AA': + stacked_structure = StartingFramework + + if stacking == 'AB1': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [2/3, 1/3, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AB2': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/2, 0, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/2, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC1': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (2/3, 1/3, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (4/3, 2/3, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC2': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 0, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 0, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AAl': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + sv = np.array(shift_vector) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos + sv)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + # Create AA tilted stacking. + if stacking == 'AAt': + cell = StartingFramework.as_dict()['lattice'] + + # Shift the cell by the tilt angle + a_cell = cell['a'] + b_cell = cell['b'] + c_cell = cell['c'] * 2 + alpha = cell['alpha'] - tilt_angle + beta = cell['beta'] - tilt_angle + gamma = cell['gamma'] + + self.cellMatrix = cellpar_to_cell([a_cell, b_cell, c_cell, alpha, beta, gamma]) + self.cellParameters = np.array([a_cell, b_cell, c_cell, alpha, beta, gamma]).astype(float) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = stacked_structure.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = stacked_structure.formula + + dist_matrix = stacked_structure.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(stacked_structure, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_refined_structure(keep_site_properties=True) + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_hxl_a_structure(self, + BB_H6: str, + BB_L2: str, + stacking: str = 'AA', + print_result: bool = True, + slab: float = 10.0, + shift_vector: list = [1.0, 1.0, 0], + tilt_angle: float = 5.0): + """Creates a COF with HXL-A network. + + The HXL-A net is composed of one hexapodal and one linear building blocks. + + Parameters + ---------- + BB_H6 : BuildingBlock, required + The BuildingBlock object of the tetrapodal Buiding Block A + BB_L2 : BuildingBlock, required + The BuildingBlock object of the tetrapodal Buiding Block B + stacking : str, optional + The stacking pattern of the COF layers (default is 'AA') + print_result : bool, optional + Parameter for the control for printing the result (default is True) + slab : float, optional + Default parameter for the interlayer slab (default is 10.0) + shift_vector: list, optional + Shift vector for the AAl and AAt stakings (defatult is [1.0,1.0,0]) + tilt_angle: float, optional + Tilt angle for the AAt staking in degrees (default is 5.0) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_H6.connectivity != 6: + self.logger.error(connectivity_error.format('A', 6, BB_H6.connectivity)) + raise BBConnectivityError(6, BB_H6.connectivity) + if BB_L2.connectivity != 2: + self.logger.error(connectivity_error.format('B', 3, BB_L2.connectivity)) + raise BBConnectivityError(2, BB_L2.connectivity) + + self.name = f'{BB_H6.name}-{BB_L2.name}-HXL_A-{stacking}' + self.topology = 'HXL_A' + self.staking = stacking + self.dimension = 2 + + self.charge = BB_H6.charge + BB_L2.charge + self.chirality = BB_H6.chirality or BB_L2.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_H6.conector, BB_L2.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_H6.conector, + BB_L2.conector)) + + # Replace "X" the building block + BB_L2.replace_X(bond_atom) + + # Remove the "X" atoms from the the building block + BB_H6.remove_X() + BB_L2.remove_X() + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = BB_H6.size[0] + BB_L2.size[0] + + # Calculate the delta size to add to the c parameter + delta_a = abs(max(np.transpose(BB_H6.atom_pos)[2])) + abs(min(np.transpose(BB_H6.atom_pos)[2])) + delta_b = abs(max(np.transpose(BB_L2.atom_pos)[2])) + abs(min(np.transpose(BB_L2.atom_pos)[2])) + + delta_max = max([delta_a, delta_b]) + + # Calculate the cell parameters + a = topology_info['a'] * size + b = topology_info['b'] * size + c = topology_info['c'] + delta_max + alpha = topology_info['alpha'] + beta = topology_info['beta'] + gamma = topology_info['gamma'] + + if self.stacking == 'A': + c = slab + + # Create the lattice + self.cellMatrix = Lattice.from_parameters(a, b, c, alpha, beta, gamma) + self.cellParameters = np.array([a, b, c, alpha, beta, gamma]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Add the building blocks to the structure + for vertice_data in topology_info['vertices']: + self.atom_types += BB_H6.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', + vertice_data['angle'], + degrees=True).as_matrix() + + rotated_pos = np.dot(BB_H6.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C1' if i == 'C' else i for i in BB_H6.atom_labels] + + # Add the building blocks to the structure + for edge_data in topology_info['edges']: + self.atom_types += BB_L2.atom_types + edge_pos = np.array(edge_data['position'])*a + + R_Matrix = R.from_euler('z', + edge_data['angle'], + degrees=True).as_matrix() + + rotated_pos = np.dot(BB_L2.atom_pos, R_Matrix) + edge_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C2' if i == 'C' else i for i in BB_L2.atom_labels] + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + # Translates the structure to the center of the cell + StartingFramework.translate_sites( + range(len(StartingFramework.as_dict()['sites'])), + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + + if stacking == 'A' or stacking == 'AA': + stacked_structure = StartingFramework + + if stacking == 'AB1': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [2/3, 1/3, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AB2': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/2, 0, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/2, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC1': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (2/3, 1/3, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (4/3, 2/3, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC2': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 0, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 0, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AAl': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + sv = np.array(shift_vector) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos + sv)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + # Create AA tilted stacking. + if stacking == 'AAt': + cell = StartingFramework.as_dict()['lattice'] + + # Shift the cell by the tilt angle + a_cell = cell['a'] + b_cell = cell['b'] + c_cell = cell['c'] * 2 + alpha = cell['alpha'] - tilt_angle + beta = cell['beta'] - tilt_angle + gamma = cell['gamma'] + + self.cellMatrix = cellpar_to_cell([a_cell, b_cell, c_cell, alpha, beta, gamma]) + self.cellParameters = np.array([a_cell, b_cell, c_cell, alpha, beta, gamma]).astype(float) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = stacked_structure.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = stacked_structure.formula + + dist_matrix = stacked_structure.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(stacked_structure, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_refined_structure(keep_site_properties=True) + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_fxt_structure(self, + BB_S4_A: str, + BB_S4_B: str, + stacking: str = 'AA', + print_result: bool = True, + slab: float = 10.0, + shift_vector: list = [1.0, 1.0, 0], + tilt_angle: float = 5.0): + """Creates a COF with FXT network. + + The FXT net is composed of two tetrapodal building blocks. + + Parameters + ---------- + BB_S4_A : BuildingBlock, required + The BuildingBlock object of the tetrapodal Buiding Block A + BB_S4_B : BuildingBlock, required + The BuildingBlock object of the tetrapodal Buiding Block B + stacking : str, optional + The stacking pattern of the COF layers (default is 'AA') + print_result : bool, optional + Parameter for the control for printing the result (default is True) + slab : float, optional + Default parameter for the interlayer slab (default is 10.0) + shift_vector: list, optional + Shift vector for the AAl and AAt stakings (defatult is [1.0,1.0,0]) + tilt_angle: float, optional + Tilt angle for the AAt staking in degrees (default is 5.0) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_S4_A.connectivity != 4: + self.logger.error(connectivity_error.format('A', 4, BB_S4_A.connectivity)) + raise BBConnectivityError(4, BB_S4_A.connectivity) + if BB_S4_B.connectivity != 4: + self.logger.error(connectivity_error.format('B', 4, BB_S4_B.connectivity)) + raise BBConnectivityError(4, BB_S4_B.connectivity) + + self.name = f'{BB_S4_A.name}-{BB_S4_B.name}-FXT-{stacking}' + self.topology = 'FXT' + self.staking = stacking + self.dimension = 2 + + self.charge = BB_S4_A.charge + BB_S4_B.charge + self.chirality = BB_S4_A.chirality or BB_S4_B.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_S4_A.conector, BB_S4_B.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_S4_A.conector, + BB_S4_B.conector)) + + # Replace "X" the building block + BB_S4_A.replace_X(bond_atom) + + # Remove the "X" atoms from the the building block + BB_S4_A.remove_X() + BB_S4_B.remove_X() + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = 2 * (BB_S4_A.size[0] + BB_S4_B.size[0]) + + # Calculate the delta size to add to the c parameter + delta_a = abs(max(np.transpose(BB_S4_A.atom_pos)[2])) + abs(min(np.transpose(BB_S4_B.atom_pos)[2])) + delta_b = abs(max(np.transpose(BB_S4_A.atom_pos)[2])) + abs(min(np.transpose(BB_S4_B.atom_pos)[2])) + + delta_max = max([delta_a, delta_b]) + + # Calculate the cell parameters + a = topology_info['a'] * size + b = topology_info['b'] * size + c = topology_info['c'] + delta_max + alpha = topology_info['alpha'] + beta = topology_info['beta'] + gamma = topology_info['gamma'] + + if self.stacking == 'A': + c = slab + + # Create the lattice + self.cellMatrix = Lattice.from_parameters(a, b, c, alpha, beta, gamma) + self.cellParameters = np.array([a, b, c, alpha, beta, gamma]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Add the first building block to the structure + vertice_data = topology_info['vertices'][0] + self.atom_types += BB_S4_A.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', vertice_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_S4_A.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C1' if i == 'C' else i for i in BB_S4_A.atom_labels] + + # Add the second building block to the structure + for vertice_data in topology_info['vertices'][1:]: + self.atom_types += BB_S4_B.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', vertice_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_S4_B.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C2' if i == 'C' else i for i in BB_S4_B.atom_labels] + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + # Translates the structure to the center of the cell + StartingFramework.translate_sites( + range(len(StartingFramework.as_dict()['sites'])), + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + + if stacking == 'A' or stacking == 'AA': + stacked_structure = StartingFramework + + if stacking == 'AB1': + + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/4, 1/4, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/4, 1/4, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AB2': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/2, 0, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/2, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC1': + + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (1/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 1/3, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 2/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 2/3, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC2': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (1/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 0, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 2/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 0, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + # Create AAl stacking. Tetragonal cell with two sheets + # per cell shifited by the shift_vector in angstroms. + if stacking == 'AAl': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + sv = np.array(shift_vector) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos + sv)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + # Create AA tilted stacking. + # Tilted tetragonal cell with two sheets per cell tilted by tilt_angle. + if stacking == 'AAt': + cell = StartingFramework.as_dict()['lattice'] + + # Shift the cell by the tilt angle + a_cell = cell['a'] + b_cell = cell['b'] + c_cell = cell['c'] * 2 + alpha = cell['alpha'] - tilt_angle + beta = cell['beta'] - tilt_angle + gamma = cell['gamma'] + + self.cellMatrix = cellpar_to_cell([a_cell, b_cell, c_cell, alpha, beta, gamma]) + self.cellParameters = np.array([a_cell, b_cell, c_cell, alpha, beta, gamma]).astype(float) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = stacked_structure.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = stacked_structure.formula + + dist_matrix = stacked_structure.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(stacked_structure, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_refined_structure(keep_site_properties=True) + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_fxt_a_structure(self, + BB_S4: str, + BB_L2: str, + stacking: str = 'AA', + c_parameter_base: float = 3.6, + print_result: bool = True, + slab: float = 10.0, + shift_vector: list = [1.0, 1.0, 0], + tilt_angle: float = 5.0): + """Creates a COF with FXT-A network. + + The FXT-A net is composed of one tetrapodal and one linear building blocks. + + Parameters + ---------- + BB_S4 : BuildingBlock, required + The BuildingBlock object of the tetrapodal Buiding Block + BB_L2 : BuildingBlock, required + The BuildingBlock object of the bipodal Buiding Block + stacking : str, optional + The stacking pattern of the COF layers (default is 'AA') + print_result : bool, optional + Parameter for the control for printing the result (default is True) + slab : float, optional + Default parameter for the interlayer slab (default is 10.0) + shift_vector: list, optional + Shift vector for the AAl and AAt stakings (defatult is [1.0,1.0,0]) + tilt_angle: float, optional + Tilt angle for the AAt staking in degrees (default is 5.0) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_S4.connectivity != 4: + self.logger.error(connectivity_error.format('A', 4, BB_S4.connectivity)) + raise BBConnectivityError(4, BB_S4.connectivity) + if BB_L2.connectivity != 2: + self.logger.error(connectivity_error.format('B', 3, BB_L2.connectivity)) + raise BBConnectivityError(2, BB_L2.connectivity) + + self.name = f'{BB_S4.name}-{BB_L2.name}-FXT_A-{stacking}' + self.topology = 'FXT_A' + self.staking = stacking + self.dimension = 2 + + self.charge = BB_S4.charge + BB_L2.charge + self.chirality = BB_S4.chirality or BB_L2.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_S4.conector, BB_L2.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_S4.conector, + BB_L2.conector)) + + # Replace "X" the building block + BB_L2.replace_X(bond_atom) + + # Remove the "X" atoms from the the building block + BB_S4.remove_X() + BB_L2.remove_X() + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = 2 * (BB_S4.size[0] + BB_L2.size[0]) + + # Calculate the delta size to add to the c parameter + delta_a = abs(max(np.transpose(BB_S4.atom_pos)[2])) + abs(min(np.transpose(BB_S4.atom_pos)[2])) + delta_b = abs(max(np.transpose(BB_L2.atom_pos)[2])) + abs(min(np.transpose(BB_L2.atom_pos)[2])) + + delta_max = max([delta_a, delta_b]) + + # Calculate the cell parameters + a = topology_info['a'] * size + b = topology_info['b'] * size + c = topology_info['c'] + delta_max + alpha = topology_info['alpha'] + beta = topology_info['beta'] + gamma = topology_info['gamma'] + + if self.stacking == 'A': + c = slab + + # Create the lattice + self.cellMatrix = Lattice.from_parameters(a, b, c, alpha, beta, gamma) + self.cellParameters = np.array([a, b, c, alpha, beta, gamma]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Add the building blocks to the structure + for vertice_data in topology_info['vertices']: + self.atom_types += BB_S4.atom_types + vertice_pos = np.array(vertice_data['position'])*a + + R_Matrix = R.from_euler('z', vertice_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_S4.atom_pos, R_Matrix) + vertice_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C1' if i == 'C' else i for i in BB_S4.atom_labels] + + # Add the building blocks to the structure + for edge_data in topology_info['edges']: + self.atom_types += BB_L2.atom_types + edge_pos = np.array(edge_data['position'])*a + + R_Matrix = R.from_euler('z', edge_data['angle'], degrees=True).as_matrix() + + rotated_pos = np.dot(BB_L2.atom_pos, R_Matrix) + edge_pos + self.atom_pos += rotated_pos.tolist() + + self.atom_labels += ['C2' if i == 'C' else i for i in BB_L2.atom_labels] + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + # Translates the structure to the center of the cell + StartingFramework.translate_sites( + range(len(StartingFramework.as_dict()['sites'])), + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + + if stacking == 'A' or stacking == 'AA': + stacked_structure = StartingFramework + + if stacking == 'AB1': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [2/3, 1/3, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AB2': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [1/2, 0, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [1/2, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC1': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (2/3, 1/3, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (4/3, 2/3, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'ABC2': + self.cellMatrix *= (1, 1, 3) + self.cellParameters *= (1, 1, 3, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + _, B_list, C_list = np.split(np.arange(len(self.atom_types)), 3) + + # Translate the second sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + B_list, + (1/3, 0, 1/3), + frac_coords=True, + to_unit_cell=True + ) + + # Translate the third sheet by the vector (2/3, 1/3, 0) to generate the B positions + stacked_structure.translate_sites( + C_list, + (2/3, 0, 2/3), + frac_coords=True, + to_unit_cell=True + ) + + if stacking == 'AAl': + self.cellMatrix *= (1, 1, 2) + self.cellParameters *= (1, 1, 2, 1, 1, 1) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + sv = np.array(shift_vector) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos + sv)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + # Create AA tilted stacking. + if stacking == 'AAt': + cell = StartingFramework.as_dict()['lattice'] + + # Shift the cell by the tilt angle + a_cell = cell['a'] + b_cell = cell['b'] + c_cell = cell['c'] * 2 + alpha = cell['alpha'] - tilt_angle + beta = cell['beta'] - tilt_angle + gamma = cell['gamma'] + + self.cellMatrix = cellpar_to_cell([a_cell, b_cell, c_cell, alpha, beta, gamma]) + self.cellParameters = np.array([a_cell, b_cell, c_cell, alpha, beta, gamma]).astype(float) + + self.atom_types = np.concatenate((self.atom_types, self.atom_types)) + self.atom_pos = np.concatenate((self.atom_pos, self.atom_pos)) + self.atom_labels = np.concatenate((self.atom_labels, self.atom_labels)) + + stacked_structure = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ) + + # Get the index of the atoms in the second sheet + B_list = np.split(np.arange(len(self.atom_types)), 2)[1] + + # Translate the second sheet by the vector [2/3, 1/3, 0.5] to generate the B positions + stacked_structure.translate_sites( + B_list, + [0, 0, 0.5], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = stacked_structure.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = stacked_structure.formula + + dist_matrix = stacked_structure.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(stacked_structure, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_refined_structure(keep_site_properties=True) + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_dia_structure(self, + BB_D41: str, + BB_D42: str, + interp_dg: str = '1', + d_param_base: float = 7.2, + print_result: bool = True, + **kwargs): + """Creates a COF with DIA network. + + The DIA net is composed of two tetrapodal tetrahedical building blocks. + + Parameters + ---------- + BB_D41 : BuildingBlock, required + The BuildingBlock object of the tetrapodal tetrahedical Buiding Block + BB_D42 : BuildingBlock, required + The BuildingBlock object of the tetrapodal tetrahedical Buiding Block + interp_dg : str, optional + The degree of interpenetration of the framework (default is '1') + d_param_base : float, optional + The base value for interlayer distance in angstroms (default is 7.2) + print_result : bool, optional + Parameter for the control for printing the result (default is True) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_D41.connectivity != 4: + self.logger.error(connectivity_error.format('A', 4, BB_D41.connectivity)) + raise BBConnectivityError(4, BB_D41.connectivity) + if BB_D42.connectivity != 4: + self.logger.error(connectivity_error.format('B', 4, BB_D42.connectivity)) + raise BBConnectivityError(4, BB_D42.connectivity) + + self.name = f'{BB_D41.name}-{BB_D42.name}-DIA-{interp_dg}' + self.topology = 'DIA' + self.staking = interp_dg + self.dimension = 3 + + self.charge = BB_D41.charge + BB_D42.charge + self.chirality = BB_D41.chirality or BB_D42.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_D41.conector, BB_D42.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_D41.conector, + BB_D42.conector)) + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = np.average(BB_D41.size) + np.average(BB_D42.size) + + # Calculate the primitive cell vector assuming tetrahedical building blocks + a_prim = np.sqrt(2)*size*np.sqrt((1 - np.cos(1.9106316646041868))) + a_conv = np.sqrt(2)*a_prim + + # Create the primitive lattice + self.cellMatrix = Lattice(a_conv/2*np.array(topology_info['lattice'])) + self.cellParameters = np.array([a_prim, a_prim, a_prim, 60, 60, 60]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Align and rotate the building block 1 to their respective positions + BB_D41.align_to(topology_info['vertices'][0]['align_v']) + + # Determine the angle that alings the X[1] to one of the vertices of the tetrahedron + vertice_pos = unit_vector(np.array([1, 0, 1])) + Q_vertice_pos = BB_D41.get_X_points()[1][1] + + rotated_list = [ + R.from_rotvec( + angle * unit_vector(topology_info['vertices'][0]['align_v']), degrees=False + ).apply(Q_vertice_pos) + for angle in np.linspace(0, 2*np.pi, 360) + ] + + # Calculate the angle between the vertice_pos and the elements of rotated_list + angle_list = [angle(vertice_pos, i) for i in rotated_list] + + rot_angle = np.linspace(0, 360, 360)[np.argmax(angle_list)] + + BB_D41.rotate_around(rotation_axis=np.array(topology_info['vertices'][0]['align_v']), + angle=rot_angle, + degree=True) + + BB_D41.shift(np.array(topology_info['vertices'][0]['position'])*a_conv) + BB_D41.remove_X() + + # Add the building block 1 to the structure + self.atom_types += BB_D41.atom_types + self.atom_pos += BB_D41.atom_pos.tolist() + self.atom_labels += ['C1' if i == 'C' else i for i in BB_D41.atom_labels] + + # Align and rotate the building block 1 to their respective positions + BB_D42.align_to(topology_info['vertices'][0]['align_v']) + + # Determine the angle that alings the X[1] to one of the vertices of the tetrahedron + vertice_pos = unit_vector(np.array([1, 0, 1])) + Q_vertice_pos = BB_D42.get_X_points()[1][1] + + rotated_list = [ + R.from_rotvec( + angle * unit_vector(topology_info['vertices'][0]['align_v']), degrees=False + ).apply(Q_vertice_pos) + for angle in np.linspace(0, 2*np.pi, 360) + ] + + # Calculate the angle between the vertice_pos and the elements of rotated_list + angle_list = [angle(vertice_pos, i) for i in rotated_list] + + rot_angle = np.linspace(0, 360, 360)[np.argmax(angle_list)] + + BB_D42.rotate_around(rotation_axis=np.array(topology_info['vertices'][0]['align_v']), + angle=rot_angle, + degree=True) + + BB_D42.atom_pos = -BB_D42.atom_pos + + BB_D42.shift(np.array(topology_info['vertices'][1]['position'])*a_conv) + + BB_D42.replace_X(bond_atom) + BB_D42.remove_X() + + # Add the building block 2 to the structure + self.atom_types += BB_D42.atom_types + self.atom_pos += BB_D42.atom_pos.tolist() + self.atom_labels += ['C2' if i == 'C' else i for i in BB_D42.atom_labels] + + atom_types, atom_labels, atom_pos = [], [], [] + for n_int in range(int(self.stacking)): + int_direction = np.array([0, 1, 0]) * d_param_base * n_int + + atom_types += self.atom_types + atom_pos += (np.array(self.atom_pos) + int_direction).tolist() + atom_labels += self.atom_labels + + self.atom_types = atom_types + self.atom_pos = atom_pos + self.atom_labels = atom_labels + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + StartingFramework.to(os.path.join(os.getcwd(), 'TESTE_DIA.cif'), fmt='cif') + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = StartingFramework.formula + + dist_matrix = StartingFramework.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(StartingFramework, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_primitive_standard_structure(keep_site_properties=True) + + dict_structure = symm.get_refined_structure(keep_site_properties=True).as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + self.cellParameters = np.array([dict_structure['lattice']['a'], + dict_structure['lattice']['b'], + dict_structure['lattice']['c'], + dict_structure['lattice']['alpha'], + dict_structure['lattice']['beta'], + dict_structure['lattice']['gamma']]).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = self.prim_structure.formula + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_dia_a_structure(self, + BB_D4: str, + BB_L2: str, + interp_dg: str = '1', + d_param_base: float = 7.2, + print_result: bool = True, + **kwargs): + """Creates a COF with DIA-A network. + + The DIA net is composed of two tetrapodal tetrahedical building blocks. + + Parameters + ---------- + BB_D4 : BuildingBlock, required + The BuildingBlock object of the tetrapodal tetrahedical Buiding Block + BB_L2 : BuildingBlock, required + The BuildingBlock object of the dipodal linear Buiding Block + interp_dg : str, optional + The degree of interpenetration of the framework (default is '1') + d_param_base : float, optional + The base value for interlayer distance in angstroms (default is 7.2) + print_result : bool, optional + Parameter for the control for printing the result (default is True) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_D4.connectivity != 4: + self.logger.error(connectivity_error.format('A', 4, BB_D4.connectivity)) + raise BBConnectivityError(4, BB_D4.connectivity) + if BB_L2.connectivity != 2: + self.logger.error(connectivity_error.format('B', 2, BB_L2.connectivity)) + raise BBConnectivityError(2, BB_L2.connectivity) + + self.name = f'{BB_D4.name}-{BB_L2.name}-DIA_A-{interp_dg}' + self.topology = 'DIA_A' + self.staking = interp_dg + self.dimension = 3 + + self.charge = BB_D4.charge + BB_L2.charge + self.chirality = BB_D4.chirality or BB_L2.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_D4.conector, BB_L2.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_D4.conector, + BB_L2.conector)) + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + size = 2 * (np.average(BB_D4.size) + np.average(BB_L2.size)) + + # Calculate the primitive cell vector assuming tetrahedical building blocks + a_prim = np.sqrt(2)*size*np.sqrt((1 - np.cos(1.9106316646041868))) + a_conv = np.sqrt(2)*a_prim + + # Create the primitive lattice + self.cellMatrix = Lattice(a_conv/2*np.array(topology_info['lattice'])) + self.cellParameters = np.array([a_prim, a_prim, a_prim, 60, 60, 60]).astype(float) + + # Create the structure + self.atom_types = [] + self.atom_labels = [] + self.atom_pos = [] + + # Align and rotate the building block 1 to their respective positions + BB_D4.align_to(topology_info['vertices'][0]['align_v']) + + # Determine the angle that alings the X[1] to one of the vertices of the tetrahedron + vertice_pos = unit_vector(np.array([1, 0, 1])) + Q_vertice_pos = BB_D4.get_X_points()[1][1] + + rotated_list = [ + R.from_rotvec( + angle * unit_vector(topology_info['vertices'][0]['align_v']), degrees=False + ).apply(Q_vertice_pos) + for angle in np.linspace(0, 2*np.pi, 360) + ] + + # Calculate the angle between the vertice_pos and the elements of rotated_list + angle_list = [angle(vertice_pos, i) for i in rotated_list] + + rot_angle = np.linspace(0, 360, 360)[np.argmax(angle_list)] + + BB_D4.rotate_around(rotation_axis=np.array(topology_info['vertices'][0]['align_v']), + angle=rot_angle, + degree=True) + + BB_D4.shift(np.array(topology_info['vertices'][0]['position'])*a_conv) + BB_D4.remove_X() + + # Add the building block 1 to the structure + self.atom_types += BB_D4.atom_types + self.atom_pos += BB_D4.atom_pos.tolist() + self.atom_labels += ['C1' if i == 'C' else i for i in BB_D4.atom_labels] + + # Add the building block 1 to the structure + self.atom_types += BB_D4.atom_types + self.atom_pos += list(-np.array(BB_D4.atom_pos) + np.array(topology_info['vertices'][1]['position'])*a_conv) + self.atom_labels += ['C1' if i == 'C' else i for i in BB_D4.atom_labels] + + # Add the building blocks to the structure + for edge_data in topology_info['edges']: + # Copy the building block 2 object + BB = copy.deepcopy(BB_L2) + + # Align, rotate and shift the building block 2 to their respective positions + BB.align_to(edge_data['align_v']) + BB.rotate_around(rotation_axis=edge_data['align_v'], + angle=edge_data['angle']) + BB.shift(np.array(edge_data['position']) * a_conv) + + # Replace "X" the building block with the correct atom dicated by the connection group + BB.replace_X(bond_atom) + BB.remove_X() + + # Update the structure + self.atom_types += BB.atom_types + self.atom_pos += BB.atom_pos.tolist() + self.atom_labels += ['C2' if i == 'C' else i for i in BB.atom_labels] + + atom_types, atom_labels, atom_pos = [], [], [] + for n_int in range(int(self.stacking)): + int_direction = np.array([0, 1, 0]) * d_param_base * n_int + + atom_types += self.atom_types + atom_pos += (np.array(self.atom_pos) + int_direction).tolist() + atom_labels += self.atom_labels + + self.atom_types = atom_types + self.atom_pos = atom_pos + self.atom_labels = atom_labels + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + StartingFramework.translate_sites( + np.ones(len(self.atom_types)).astype(int).tolist(), + [0, 0, 0], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + self.cellParameters = np.array([dict_structure['lattice']['a'], + dict_structure['lattice']['b'], + dict_structure['lattice']['c'], + dict_structure['lattice']['alpha'], + dict_structure['lattice']['beta'], + dict_structure['lattice']['gamma']]).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = StartingFramework.formula + + StartingFramework.to(os.path.join(os.getcwd(), 'TESTE_DIA-A.cif'), fmt='cif') + + dist_matrix = StartingFramework.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(StartingFramework, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_primitive_standard_structure(keep_site_properties=True) + + dict_structure = symm.get_refined_structure(keep_site_properties=True).as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + self.cellParameters = np.array([dict_structure['lattice']['a'], + dict_structure['lattice']['b'], + dict_structure['lattice']['c'], + dict_structure['lattice']['alpha'], + dict_structure['lattice']['beta'], + dict_structure['lattice']['gamma']]).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = self.prim_structure.formula + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] + + def create_bor_structure(self, + BB_D4: str, + BB_T3: str, + interp_dg: str = '1', + d_param_base: float = 7.2, + print_result: bool = True, + **kwargs): + """Creates a COF with BOR network. + + The DIA net is composed of one tetrapodal tetrahedical building block and + one tripodal triangular building block. + + Parameters + ---------- + BB_D4 : BuildingBlock, required + The BuildingBlock object of the tetrapodal tetrahedical Buiding Block + BB_T3 : BuildingBlock, required + The BuildingBlock object of the tripodal triangular Buiding Block + interp_dg : str, optional + The degree of interpenetration of the framework (default is '1') + d_param_base : float, optional + The base value for interlayer distance in angstroms (default is 7.2) + print_result : bool, optional + Parameter for the control for printing the result (default is True) + + Returns + ------- + list + A list of strings containing: + 1. the structure name, + 2. lattice type, + 3. hall symbol of the cristaline structure, + 4. space group, + 5. number of the space group, + 6. number of operation symmetry + """ + connectivity_error = 'Building block {} must present connectivity {} not {}' + if BB_D4.connectivity != 4: + self.logger.error(connectivity_error.format('A', 4, BB_D4.connectivity)) + raise BBConnectivityError(4, BB_D4.connectivity) + if BB_T3.connectivity != 3: + self.logger.error(connectivity_error.format('B', 3, BB_T3.connectivity)) + raise BBConnectivityError(3, BB_T3.connectivity) + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + self.name = f'{BB_D4.name}-{BB_T3.name}-BOR-{interp_dg}' + self.topology = 'BOR' + self.staking = interp_dg + self.dimension = 3 + + self.charge = BB_D4.charge + BB_T3.charge + self.chirality = BB_D4.chirality or BB_T3.chirality + + self.logger.debug(f'Starting the creation of {self.name}') + + # Detect the bond atom from the connection groups type + bond_atom = get_bond_atom(BB_D4.conector, BB_T3.conector) + + self.logger.debug('{} detected as bond atom for groups {} and {}'.format(bond_atom, + BB_D4.conector, + BB_T3.conector)) + + # Get the topology information + topology_info = TOPOLOGY_DICT[self.topology] + + # Measure the base size of the building blocks + d_size = (np.array(BB_D4.size).mean() + np.array(BB_T3.size).mean()) + + # Calculate the primitive cell vector assuming tetrahedical building blocks + a_conv = np.sqrt(6) * d_size + + # Create the primitive lattice + self.cellMatrix = Lattice(a_conv * np.array(topology_info['lattice'])) + self.cellParameters = np.array([a_conv, a_conv, a_conv, 90, 90, 90]).astype(float) + + # Create the structure + atom_types = [] + atom_labels = [] + atom_pos = [] + + for D_site in topology_info['vertices']: + D4 = BB_D4.copy() + D4.align_to( + np.array(D_site['align_v']) + ) + + D4.rotate_around( + rotation_axis=D_site['align_v'], + angle=D_site['angle']) + + D4.shift(np.array(D_site['position'])*a_conv) + + atom_types += D4.atom_types + atom_pos += D4.atom_pos.tolist() + atom_labels += D4.atom_labels.tolist() + + # Translate all atoms to inside the cell + for i, pos in enumerate(atom_pos): + for j, coord in enumerate(pos): + if coord < 0: + atom_pos[i][j] += a_conv + + X_pos = [atom_pos[i] for i in np.where(np.array(atom_types) == 'X')[0]] + + T_site = topology_info['edges'][0] + + _, X = BB_T3.get_X_points() + BB_T3.rotate_around([0, 0, 1], T_site['angle'], True) + + R_matrix = rotation_matrix_from_vectors([0, 0, 1], + T_site['align_v']) + + BB_T3.atom_pos = np.dot(BB_T3.atom_pos, R_matrix.T) + + BB_T3.replace_X('O') + + # Get the 3 atoms that are closer to T_site['position'])*a_conv + X_pos_temp = sorted(X_pos, key=lambda x: np.linalg.norm(x - np.array(T_site['position'])*a_conv)) + + X_center = np.array(X_pos_temp[:3]).mean(axis=0) + + BB_T3.shift(X_center) + + atom_types += BB_T3.atom_types + atom_pos += BB_T3.atom_pos.tolist() + atom_labels += BB_T3.atom_labels.tolist() + + T4 = BB_T3.copy() + T4.rotate_around([0, 0, 1], 180, True) + + atom_types += T4.atom_types + atom_pos += T4.atom_pos.tolist() + atom_labels += T4.atom_labels.tolist() + + T2 = BB_T3.copy() + T2.rotate_around([0, 0, 1], 90, True) + T2.rotate_around([1, 0, 0], -90, True) + + atom_types += T2.atom_types + atom_pos += T2.atom_pos.tolist() + atom_labels += T2.atom_labels.tolist() + + T3 = BB_T3.copy() + T3.rotate_around([0, 0, 1], -90, True) + + T3.atom_pos *= np.array([1, 1, -1]) + + atom_types += T3.atom_types + atom_pos += T3.atom_pos.tolist() + atom_labels += T3.atom_labels.tolist() + + # Translate all atoms to inside the cell + for i, pos in enumerate(atom_pos): + for j, coord in enumerate(pos): + if coord < 0: + atom_pos[i][j] += a_conv + + # Remove the X atoms from the list + X_index = np.where(np.array(atom_types) == 'X')[0] + + self.atom_types = [atom_types[i] for i in range(len(atom_types)) if i not in X_index] + self.atom_pos = [atom_pos[i] for i in range(len(atom_pos)) if i not in X_index] + self.atom_labels = [atom_labels[i] for i in range(len(atom_labels)) if i not in X_index] + + atom_types, atom_labels, atom_pos = [], [], [] + for n_int in range(int(self.stacking)): + int_direction = np.array([0, 1, 0]) * d_param_base * n_int + + atom_types += self.atom_types + atom_pos += (np.array(self.atom_pos) + int_direction).tolist() + atom_labels += self.atom_labels + + self.atom_types = atom_types + self.atom_pos = atom_pos + self.atom_labels = atom_labels + + StartingFramework = Structure( + self.cellMatrix, + self.atom_types, + self.atom_pos, + coords_are_cartesian=True, + site_properties={'source': self.atom_labels} + ).get_sorted_structure() + + StartingFramework.translate_sites( + np.ones(len(self.atom_types)).astype(int).tolist(), + [0, 0, 0], + frac_coords=True, + to_unit_cell=True + ) + + dict_structure = StartingFramework.as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + self.cellParameters = np.array([dict_structure['lattice']['a'], + dict_structure['lattice']['b'], + dict_structure['lattice']['c'], + dict_structure['lattice']['alpha'], + dict_structure['lattice']['beta'], + dict_structure['lattice']['gamma']]).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = StartingFramework.formula + + StartingFramework.to('TESTE_BOR.cif', fmt='cif') + + dist_matrix = StartingFramework.distance_matrix + + # Check if there are any atoms closer than 0.8 A + for i in range(len(dist_matrix)): + for j in range(i+1, len(dist_matrix)): + if dist_matrix[i][j] < self.dist_threshold: + raise BondLenghError(i, j, dist_matrix[i][j], self.dist_threshold) + + # Get the simmetry information of the generated structure + symm = SpacegroupAnalyzer(StartingFramework, + symprec=self.symm_tol, + angle_tolerance=self.angle_tol) + + try: + self.prim_structure = symm.get_primitive_standard_structure(keep_site_properties=True) + + dict_structure = symm.get_refined_structure(keep_site_properties=True).as_dict() + + self.cellMatrix = np.array(dict_structure['lattice']['matrix']).astype(float) + self.cellParameters = np.array([dict_structure['lattice']['a'], + dict_structure['lattice']['b'], + dict_structure['lattice']['c'], + dict_structure['lattice']['alpha'], + dict_structure['lattice']['beta'], + dict_structure['lattice']['gamma']]).astype(float) + + self.atom_types = [i['label'] for i in dict_structure['sites']] + self.atom_pos = [i['xyz'] for i in dict_structure['sites']] + self.atom_labels = [i['properties']['source'] for i in dict_structure['sites']] + self.n_atoms = len(dict_structure['sites']) + self.composition = self.prim_structure.formula + + self.logger.debug(self.prim_structure) + + self.lattice_type = symm.get_lattice_type() + self.space_group = symm.get_space_group_symbol() + self.space_group_n = symm.get_space_group_number() + + symm_op = symm.get_point_group_operations() + self.hall = symm.get_hall() + + except Exception as e: + self.logger.exception(e) + + self.lattice_type = 'Triclinic' + self.space_group = 'P1' + self.space_group_n = '1' + + symm_op = [1] + self.hall = 'P 1' + + symm_text = get_framework_symm_text(self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)) + + self.logger.info(symm_text) + + return [self.name, + str(self.lattice_type), + str(self.hall[0:2]), + str(self.space_group), + str(self.space_group_n), + len(symm_op)] diff --git a/build/lib/io_tools.py b/build/lib/io_tools.py new file mode 100644 index 00000000..e0d79d2c --- /dev/null +++ b/build/lib/io_tools.py @@ -0,0 +1,1287 @@ +# -*- coding: utf-8 -*- +# Created by Felipe Lopes de Oliveira +# Distributed under the terms of the MIT License. + +""" +This module contains tools for input and output file manipulation used by pyCOFBuilder. +""" + +import os +from datetime import date +import numpy as np + +from pymatgen.io.cif import CifParser + +import simplejson +from pycofbuilder.tools import (elements_dict, + cell_to_cellpar, + cellpar_to_cell, + get_fractional_to_cartesian_matrix, + get_cartesian_to_fractional_matrix, + get_kgrid, + formula_from_atom_list, + smiles_to_xsmiles, + cell_to_ibrav) + + +def save_csv(path, file_name, data, delimiter=',', head=False): + """ + Saves a file in format `.csv`. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the `csv` file. Does not neet to contain the `.csv` extention. + data : list + Data to be saved. + delimiter: str + Delimiter of the columns. `,` is the default. + head : str + Names of the columns. + """ + + # Remove the extention if exists + file_name = file_name.split('.')[0] + file_name = os.path.join(path, file_name + '.csv') + + file_temp = open(file_name, 'w') + if head is not False: + file_temp.write(head) + + for i in range(len(data)): + file_temp.write(delimiter.join([str(j) for j in data[i]]) + '\n') + + file_temp.close() + + +def read_xyz(path, file_name): + """ + Reads a file in format `.xyz` from the `path` given and returns + a list containg the N atom labels and a Nx3 array contaning + the atoms coordinates. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the `xyz` file. Does not neet to contain the `.xyz` extention. + + Returns + ------- + atom_labels : list + List of strings containing containg the N atom labels. + atom_pos : numpy array + Nx3 array contaning the atoms coordinates + """ + + # Remove the extention if exists + file_name = file_name.split('.')[0] + + if os.path.exists(os.path.join(path, file_name + '.xyz')): + temp_file = open(os.path.join(path, file_name + '.xyz'), 'r').readlines() + + atoms = [i.split() for i in temp_file[2:]] + + atom_labels = [i[0] for i in atoms if len(i) > 1] + atom_pos = np.array([[float(i[1]), float(i[2]), float(i[3])] for i in atoms if len(i) > 1]) + + return atom_labels, atom_pos + else: + print(f'File {file_name} not found!') + return None + + +def read_pdb(path, file_name): + """ + Reads a file in format `.pdb` from the `path` given and returns + a list containg the N atom labels and a Nx3 array contaning + the atoms coordinates. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the `pdb` file. Does not neet to contain the `.pdb` extention. + + Returns + ------- + atom_labels : list + List of strings containing containg the N atom labels. + atom_pos : numpy array + Nx3 array contaning the atoms coordinates + """ + + # Remove the extention if exists + file_name = file_name.split('.')[0] + + if not os.path.exists(os.path.join(path, file_name + '.pdb')): + raise FileNotFoundError(f'File {file_name} not found!') + + temp_file = open(os.path.join(path, file_name + '.pdb'), 'r').read().splitlines() + + cellParameters = np.array([i.split()[1:] for i in temp_file if 'CRYST1' in i][0]).astype(float) + + AtomTypes = [i.split()[2] for i in temp_file if 'ATOM' in i] + CartPos = np.array([i.split()[4:7] for i in temp_file if 'ATOM' in i]).astype(float) + + return cellParameters, AtomTypes, CartPos + + +def read_gjf(path, file_name): + """ + Reads a file in format `.gjf` from the `path` given and returns + a list containg the N atom labels and a Nx3 array contaning + the atoms coordinates. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the `gjf` file. Does not neet to contain the `.gjf` extention. + + Returns + ------- + atom_labels : list + List of strings containing containg the N atom labels. + atom_pos : numpy array + Nx3 array contaning the atoms coordinates + """ + # Remove the extention if exists + file_name = file_name.split('.')[0] + + if os.path.exists(os.path.join(path, file_name + '.gjf')): + + temp_file = open(os.path.join(path, file_name + '.gjf'), 'r').readlines() + temp_file = [i.split() for i in temp_file if i != '\n'] + + atoms = [i for i in temp_file if i[0] in elements_dict()] + + atom_labels = [i[0] for i in atoms] + atom_pos = np.array([[float(i[1]), float(i[2]), float(i[3])] for i in atoms]) + + return atom_labels, atom_pos + else: + print(f'File {file_name} not found!') + return None + + +def read_cif(path, file_name): + """ + Reads a file in format `.cif` from the `path` given and returns + a list containg the N atom labels and a Nx3 array contaning + the atoms coordinates. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the `cif` file. Does not neet to contain the `.cif` extention. + + Returns + ------- + cell : numpy array + 3x3 array contaning the cell vectors. + atom_labels : list + List of strings containing containg the N atom labels. + atom_pos : numpy array + Nx3 array contaning the atoms coordinates + charges : list + List of strings containing containg the N atom partial charges. + """ + + # Remove the extention if exists + file_name = file_name.split('.')[0] + + if os.path.exists(os.path.join(path, file_name + '.cif')): + + temp_file = open(os.path.join(path, file_name + '.cif'), 'r').readlines() + cell = [] + atom_label = [] + atom_pos = [] + charges = [] + has_charges = False + for i in temp_file: + if 'cell_length_a' in i: + cell += [float(i.split()[-1])] + if 'cell_length_b' in i: + cell += [float(i.split()[-1])] + if 'cell_length_c' in i: + cell += [float(i.split()[-1])] + if 'cell_angle_alpha' in i: + cell += [float(i.split()[-1])] + if '_cell_angle_beta' in i: + cell += [float(i.split()[-1])] + if '_cell_angle_gamma' in i: + cell += [float(i.split()[-1])] + if '_atom_site_charge' in i: + has_charges = True + + for i in temp_file: + line = i.split() + if len(line) > 1 and line[0] in elements_dict().keys(): + atom_label += [line[0]] + atom_pos += [[float(j) for j in line[2:5]]] + if has_charges: + charges += [float(line[-1])] + cell = cellpar_to_cell(cell) + + return cell, atom_label, atom_pos, charges + else: + print(f'File {file_name} not found!') + return None + + +def save_xsf(path: str = None, + file_name: str = None, + cell: np.ndarray = np.eye(3), + atom_types: list = None, + atom_pos: list = None, + atom_labels: list = None, + frac_coords: bool = False): + """ + Save a file in format `.xsf` on the `path`. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.xsf` extention. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + atom_label : list + List of strings containing containg the N atom label. + atom_pos : list + Nx3 array contaning the atoms coordinates. + """ + + file_name = file_name.split('.')[0] + + if len(cell) == 6: + cell = cellpar_to_cell(cell) + + if frac_coords: + # Convert to fractional coordinates + frac_matrix = get_fractional_to_cartesian_matrix(*cell_to_cellpar(cell)) + atom_pos = [np.dot(frac_matrix, [i[0], i[1], i[2]]) for i in atom_pos] + + xsf_file = open(os.path.join(path, file_name + '.xsf'), 'w') + xsf_file.write(' CRYSTAL\n') + xsf_file.write(' PRIMVEC\n') + + for i in range(len(cell)): + xsf_file.write(f' {cell[i][0]:>15.9f} {cell[i][1]:>15.9f} {cell[i][2]:>15.9f}\n') + + xsf_file.write(' PRIMCOORD\n') + xsf_file.write(f' {len(atom_pos)} 1\n') + + for i in range(len(atom_pos)): + xsf_file.write('{:3s} {:>15.9f} {:>15.9f} {:>15.9f}\n'.format(atom_types[i], + atom_pos[i][0], + atom_pos[i][1], + atom_pos[i][2])) + + xsf_file.close() + + +def save_pqr(path: str = None, + file_name: str = None, + cell: np.ndarray = np.eye(3), + atom_types: list = None, + atom_pos: list = None, + atom_labels: list = None, + partial_charges: list = None, + frac_coords: bool = False): + """ + Save a file in format `.pqr` on the `path`. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.pqr` extention. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + atom_label : list + List of strings containing containg the N atom partial charges. + atom_pos : list + Nx3 array contaning the atoms coordinates. + partial_charges: list + List of strings containing containg the N atom partial charges. + """ + + file_name = file_name.split('.')[0] + + if len(cell) == 3: + cell = cell_to_cellpar(cell) + + if frac_coords: + # Convert to fractional coordinates + frac_matrix = get_fractional_to_cartesian_matrix(*cell) + atom_pos = [np.dot(frac_matrix, [i[0], i[1], i[2]]) for i in atom_pos] + + pqr_file = open(os.path.join(path, file_name + '.pqr'), 'w') + pqr_file.write(f'TITLE {file_name} \n') + pqr_file.write('REMARK 4\n') + pqr_file.write('CRYST1{:>9.3f}{:>9.3f}{:>9.3f}{:>7.2f}{:>7.2f}{:>7.2f} P1\n'.format(cell[0], + cell[1], + cell[2], + cell[3], + cell[4], + cell[5])) + + if partial_charges is None: + atom_line = 'ATOM {:>4} {:>2} MOL A 0 {:>8.3f}{:>8.3f}{:>8.3f} {:>15}\n' + for i in range(len(atom_pos)): + pqr_file.write(atom_line.format(i + 1, + atom_types[i], + atom_pos[i][0], + atom_pos[i][1], + atom_pos[i][2], + atom_types[i])) + else: + atom_line = 'ATOM {:>4} {:>2} MOL A 0 {:>8.3f}{:>8.3f}{:>8.3f}{:>8.5f} {:>15}\n' + for i in range(len(atom_pos)): + pqr_file.write(atom_line.format(i + 1, + atom_types[i], + atom_pos[i][0], + atom_pos[i][1], + atom_pos[i][2], + partial_charges[i], + atom_types[i])) + + pqr_file.close() + + +def save_pdb(path: str = None, + file_name: str = None, + cell: np.ndarray = np.eye(3), + atom_types: list = None, + atom_pos: list = None, + atom_labels: list = None, + frac_coords: bool = False): + """ + Save a file in format `.pdb` on the `path`. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.pdb` extention. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + atom_label : list + List of strings containing containg the N atom partial charges. + atom_pos : list + Nx3 array contaning the atoms coordinates in cartesian form. + """ + + file_name = file_name.split('.')[0] + + if len(cell) == 3: + cell = cell_to_cellpar(cell) + + if frac_coords: + # Convert to fractional coordinates + frac_matrix = get_fractional_to_cartesian_matrix(*cell) + atom_pos = [np.dot(frac_matrix, [i[0], i[1], i[2]]) for i in atom_pos] + + pdb_file = open(os.path.join(path, file_name + '.pdb'), 'w') + pdb_file.write(f'TITLE {file_name} \n') + pdb_file.write('REMARK pyCOFBuilder\n') + pdb_file.write('CRYST1{:>9.3f}{:>9.3f}{:>9.3f}{:>7.2f}{:>7.2f}{:>7.2f} P1\n'.format(cell[0], + cell[1], + cell[2], + cell[3], + cell[4], + cell[5])) + + atom_line = 'ATOM {:>4} {:>2} MOL {:>13.3f}{:>8.3f}{:>8.3f} 1.00 0.00 {:>11}\n' + + for i in range(len(atom_pos)): + pdb_file.write(atom_line.format(i+1, + atom_types[i], + atom_pos[i][0], + atom_pos[i][1], + atom_pos[i][2], + atom_types[i])) + + pdb_file.close() + + +def save_gjf(path: str = None, + file_name: str = None, + cell: list = None, + atom_types: list = None, + atom_pos: list = None, + atom_labels: list = None, + frac_coords: bool = False, + text: str = 'opt pm6'): + + """ + Save a file in format `.gjf` on the `path`. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.gjf` extention. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + atom_label : list + List of strings containing containg the N atom partial charges. + atom_pos : list + Nx3 array contaning the atoms coordinates. + text : str + Parameters for Gaussian calculations. + """ + + if len(cell) == 6: + cell = cellpar_to_cell(cell) + + if frac_coords: + # Convert to fractional coordinates + frac_matrix = get_fractional_to_cartesian_matrix(*cell_to_cellpar(cell)) + atom_pos = [np.dot(frac_matrix, [i[0], i[1], i[2]]) for i in atom_pos] + + file_name = file_name.split('.')[0] + + temp_file = open(os.path.join(path, file_name + '.gjf'), 'w') + temp_file.write(f'%chk={file_name}.chk \n') + temp_file.write(f'# {text}\n') + temp_file.write('\n') + temp_file.write(f'{file_name}\n') + temp_file.write('\n') + temp_file.write('0 1 \n') + + for i in range(len(atom_types)): + temp_file.write('{:<5s}{:>15.7f}{:>15.7f}{:>15.7f}\n'.format(atom_types[i], + atom_pos[i][0], + atom_pos[i][1], + atom_pos[i][2])) + if cell is not None: + for i in range(len(cell)): + temp_file.write('Tv {:>15.7f}{:>15.7f}{:>15.7f}\n'.format(*cell[i])) + + temp_file.write('\n\n') + temp_file.close() + + +def save_xyz(path: str = None, + file_name: str = None, + cell: np.ndarray = np.eye(3), + atom_types: list = None, + atom_pos: list = None, + atom_labels: list = None, + frac_coords: bool = False): + """ + Save a file in format `.xyz` on the `path`. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.xyz` extention. + atom_types : list + List of strings containing containg the N atom types + atom_pos : list + Nx3 array contaning the atoms coordinates. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + """ + + if len(cell) == 3: + cell = cell_to_cellpar(cell) + + if frac_coords: + # Convert to fractional coordinates + frac_matrix = get_fractional_to_cartesian_matrix(cell) + atom_pos = [np.dot(frac_matrix, [i[0], i[1], i[2]]) for i in atom_pos] + + file_name = file_name.split('.')[0] + + temp_file = open(os.path.join(path, file_name + '.xyz'), 'w') + temp_file.write(f'{len(atom_types)}\n') + + if cell is None: + temp_file.write(f'{file_name}\n') + else: + temp_file.write(f'{cell[0]} {cell[1]} {cell[2]} {cell[3]} {cell[4]} {cell[5]}\n') + + for i in range(len(atom_types)): + temp_file.write('{:<5s}{:>15.7f}{:>15.7f}{:>15.7f}\n'.format(atom_types[i], + atom_pos[i][0], + atom_pos[i][1], + atom_pos[i][2])) + + temp_file.close() + + +def save_turbomole(path: str = None, + file_name: str = None, + cell: np.ndarray = np.eye(3), + atom_types: list = None, + atom_pos: list = None, + atom_labels: list = None, + frac_coords: bool = False): + """Save the structure in Turbomole .coord format + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.coord` extention. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + atom_label : list + List of strings containing containg the N atom partial charges. + atom_pos : list + Nx3 array contaning the atoms coordinates. + """ + + if cell.shape == (3, 3): + cell = cell_to_cellpar(cell) + + if frac_coords: + # Convert to fractional coordinates + frac_matrix = get_fractional_to_cartesian_matrix(*cell) + atom_pos = [np.dot(frac_matrix, [i[0], i[1], i[2]]) for i in atom_pos] + + with open(os.path.join(path, file_name + '.coord'), 'w') as temp_file: + temp_file.write('$coord angs\n') + + for i in range(len(atom_types)): + temp_file.write('{:>15.7f}{:>15.7f}{:>15.7f} {:<5s}\n'.format(atom_pos[i][0], + atom_pos[i][1], + atom_pos[i][2], + atom_types[i])) + + temp_file.write('$periodic 3\n') + temp_file.write('$cell\n') + temp_file.write('{} {} {} {} {} {}\n'.format(*cell)) + temp_file.write('$opt\n') + temp_file.write(' engine=inertial\n') + temp_file.write('$end\n') + + +def save_vasp(path: str = None, + file_name: str = None, + cell: np.ndarray = np.eye(3), + atom_types: list = None, + atom_pos: list = None, + atom_labels: list = None, + frac_coords: bool = False): + """Save the structure in VASP .vasp format + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.vasp` extention. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + atom_label : list + List of strings containing containg the N atom partial charges. + atom_pos : list + Nx3 array contaning the atoms coordinates. + coords_are_cartesian : bool + If True, the coordinates are in cartesian coordinates. + """ + + if cell.shape == 6: + cell = cellpar_to_cell(cell) + + unique_atoms = [] + for i in atom_types: + if i not in unique_atoms: + unique_atoms.append(i) + + composition_dict = {i: atom_types.count(i) for i in unique_atoms} + + with open(os.path.join(path, file_name + '.vasp'), 'w') as temp_file: + temp_file.write(f'{file_name}\n') + temp_file.write('1.0\n') + + for i in range(3): + temp_file.write('{:>15.7f}{:>15.7f}{:>15.7f}\n'.format(cell[i][0], + cell[i][1], + cell[i][2])) + + temp_file.write(' '.join(composition_dict.keys()) + '\n') + temp_file.write(' '.join([str(i) for i in composition_dict.values()]) + '\n') + + if frac_coords: + temp_file.write('Direct\n') + else: + temp_file.write('Cartesian\n') + + for i in range(len(atom_types)): + temp_file.write('{:>15.7f}{:>15.7f}{:>15.7f} {:<5s}\n'.format(atom_pos[i][0], + atom_pos[i][1], + atom_pos[i][2], + atom_types[i])) + + +def save_qe(path: str = None, + file_name: str = None, + cell: np.ndarray = np.eye(3), + atom_types: list = None, + atom_pos: list = None, + atom_labels: list = None, + frac_coords: bool = False, + calc_type: str = 'scf'): + ''' + Save the structure in Quantum Espresso .pwscf format. + + The `input_dict` can be used to specify the input parameters for the + QuantumESPRESSO calculation. + + This dictionary must contain the keys: `control`, `system`, `electrons`, and `ions`. + Each of these keys must contain a dictionary with the corresponding input parameters. + This dictionary can contain the kpoints item, with the kpoints grid as a list of 3 integers. + Additionally, it can contain the kspacing item, with the kpoints spacing as a float. In this + case the kpoints grid will be calculated automatically. By default, the kspacing is set to 0.3. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.pwscf` extention. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + atom_label : list + List of strings containing containg the N atom labels. + atom_pos : list + Nx3 array contaning the atoms coordinates. + frac_coords : bool + If True, the coordinates are in fractional coordinates. + calc_type : str + Type of pw.x calculation. Can be 'scf', 'nscf', 'bands', and 'vc-relax'. + ''' + + if len(cell) == 6: + cell_matrix = cellpar_to_cell(cell) + else: + cell_matrix = cell + + ibrav_dict = cell_to_ibrav(cell_matrix) + + input_dict = {} + + input_dict['control'] = { + 'prefix': f"'{file_name}'", + 'calculation': f"{calc_type}", + 'restart_mode': "'from_scratch'", + 'wf_collect': '.true.', + 'pseudo_dir': "'$PSEUDO_DIR'", + 'outdir': "'$SCRATCH_DIR'", + 'verbosity': "'high'", + 'tstress': '.true.', + 'tprnfor': '.true.', + 'etot_conv_thr': '1.0d-5', + 'forc_conv_thr': '1.0d-6', + 'nstep': 1000} + + input_dict['system'] = { + 'nat': len(atom_types), + 'ntyp': len(set(atom_types)), + 'ecutwfc': 40, + 'ecutrho': 360, + 'vdw_corr': "'grimme-d3'", + 'occupations': "'smearing'", + **ibrav_dict} + + input_dict['electrons'] = { + 'conv_thr': 1.0e-9, + 'electron_maxstep': 100, + 'mixing_beta': 0.3} + + if calc_type == 'vc-relax': + + input_dict['ions'] = { + 'ion_dynamics': "'bfgs'"} + + input_dict['cell'] = { + 'cell_dynamics': "'bfgs'", + 'cell_dofree': "'all'"} + + # If the kpoints grid is not specified, calculate it automatically + if 'k_points' not in input_dict.keys(): + if 'kspacing' not in input_dict.keys(): + input_dict['kspacing'] = 0.3 + input_dict['kpoints'] = get_kgrid(cell_matrix, input_dict['kspacing']) + + with open(os.path.join(path, file_name + '.pwscf'), 'w') as f: + f.write('&CONTROL\n') + for key in input_dict['control']: + f.write(f" {key} = {input_dict['control'][key]}\n") + f.write('/\n\n') + + f.write('&SYSTEM\n') + for key in input_dict['system']: + f.write(f" {key} = {input_dict['system'][key]}\n") + f.write('/\n\n') + + f.write('&ELECTRONS\n') + for key in input_dict['electrons']: + f.write(f" {key} = {input_dict['electrons'][key]}\n") + f.write('/\n\n') + + if calc_type == 'vc-relax': + f.write('&IONS\n') + for key in input_dict['ions']: + f.write(f" {key} = {input_dict['ions'][key]}\n") + f.write('/\n\n') + + f.write('&CELL\n') + for key in input_dict['cell']: + f.write(f" {key} = {input_dict['cell'][key]}\n") + f.write('/\n\n') + + f.write('ATOMIC_SPECIES\n') + for atom in set(atom_types): + f.write(f" {atom} {elements_dict()[atom]:>9.5f} {atom}.PSEUDO.UPF\n") + f.write('\n') + + # f.write('CELL_PARAMETERS (angstrom) \n') + # for v in cell_matrix: + # f.write(f'{v[0]:>15.9f} {v[1]:>15.9f} {v[2]:>15.9f}\n') + # f.write('\n') + + if frac_coords: + coords_type = 'crystal' + else: + coords_type = 'angstrom' + + f.write(f'ATOMIC_POSITIONS ({coords_type})\n') + + for i, atom in enumerate(atom_pos): + f.write('{:<5s}{:>15.9f}{:>15.9f}{:>15.9f} ! {:5}\n'.format(atom_types[i], + atom[0], + atom[1], + atom[2], + atom_labels[i])) + + f.write('\n') + f.write('K_POINTS automatic\n') + f.write(' {} {} {} 1 1 1\n'.format(*input_dict['kpoints'])) + + +def convert_cif_2_qe(out_path, file_name): + """ + Convert a cif file to a Quantum Espresso input file + + Parameters + ---------- + out_path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.cif` extention. + """ + + cell, atom_labels, atom_pos, _ = read_cif(out_path, file_name, has_charges=False) + + print(cell, atom_labels, atom_pos) + + save_qe(out_path, + file_name, + cell, + atom_labels, + atom_pos, + coords_are_cartesian=True, + supercell=False, + angs=False, + ecut=40, + erho=360, + k_dist=0.3) + + +def save_json(path, file_name, cell, atom_types, atom_pos, atom_labels, frac_coords=False): + """ + Save a file in format `.json` on the `path`. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.cif` extention. + atom_label : list + List of strings containing containg the N atom partial charges. + atom_pos : list + Nx3 array contaning the atoms coordinates. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + """ + + file_name = file_name.split('.')[0] + + cof_json = create_COF_json(file_name) + + if len(cell) == 3: + cell_par = cell_to_cellpar(np.array(cell)).tolist() + cell_matrix = np.array(cell).astype(float).tolist() + + if len(cell) == 6: + cell_par = np.array(cell).astype(float).tolist() + cell_matrix = cellpar_to_cell(cell_par).tolist() + + cof_json['system']['geo_opt'] = False + + cof_json['geometry']['cell_matrix'] = cell_matrix + cof_json['geometry']['cell_parameters'] = cell_par + cof_json['geometry']['atom_labels'] = list(atom_types) + cof_json['geometry']['atom_pos'] = list(atom_pos) + + write_json(path, file_name, cof_json) + + +def save_chemjson(path, + file_name, + cell, + atom_types, + atom_pos, + atom_labels, + frac_coords=False): + """ + Save a file in format `.json` on the `path`. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.cif` extention. + atom_label : list + List of strings containing containg the N atom partial charges. + atom_pos : list + Nx3 array contaning the atoms coordinates. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + """ + + file_name = file_name.split('.')[0] + if len(cell) == 6: + CellParameters = cell + CellMatrix = None + if len(cell) == 3: + CellParameters = None + CellMatrix = cell + + chemJSON = create_structure_CJSON(StructureName=file_name.split('.')[0], + CellParameters=CellParameters, + CellMatrix=CellMatrix, + AtomTypes=atom_types, + AtomPositions=atom_pos, + AtomLabels=atom_labels, + CartesianPositions=not frac_coords) + + write_json(path, file_name, chemJSON) + + +def save_cif(path, + file_name, + cell, + atom_types, + atom_pos, + atom_labels=None, + partial_charges=False, + frac_coords=False): + """ + Save a file in format `.cif` on the `path`. + + Parameters + ---------- + path : str + Path to the file. + file_name : str + Name of the file. Does not neet to contain the `.cif` extention. + atom_label : list + List of strings containing containg the N atom partial charges. + atom_pos : list + Nx3 array contaning the atoms coordinates. + cell : numpy array + Can be a 3x3 array contaning the cell vectors or a list with the 6 cell parameters. + """ + + file_name = file_name.split('.')[0] + + if len(cell) == 3: + a, b, c, alpha, beta, gamma = cell_to_cellpar(cell) + if len(cell) == 6: + a, b, c, alpha, beta, gamma = cell + + if atom_labels is None: + atom_labels = [''] * len(atom_types) + + cif_text = f"""\ +data_{file_name} + +_audit_creation_date {date.today().strftime("%Y-%d-%m")} +_audit_creation_method pyCOFBuilder +_audit_author_name 'Felipe Lopes de Oliveira' + +_chemical_name_common '{file_name}' +_cell_length_a {a:>10.6f} +_cell_length_b {b:>10.6f} +_cell_length_c {c:>10.6f} +_cell_angle_alpha {alpha:>6.2f} +_cell_angle_beta {beta:>6.2f} +_cell_angle_gamma {gamma:>6.2f} +_space_group_name_H-M_alt 'P 1' +_space_group_IT_number 1 + +loop_ +_symmetry_equiv_pos_as_xyz + 'x, y, z' + +loop_ + _atom_site_label + _atom_site_type_symbol + _atom_site_fract_x + _atom_site_fract_y + _atom_site_fract_z +""" + + if partial_charges is not False: + cif_text += ' _atom_site_charge\n' + + if frac_coords is False: + # Convert to fractional coordinates + frac_matrix = get_cartesian_to_fractional_matrix(a, b, c, alpha, beta, gamma) + atom_pos = [np.dot(frac_matrix, [i[0], i[1], i[2]]) for i in atom_pos] + + for i in range(len(atom_pos)): + u, v, w = atom_pos[i][0], atom_pos[i][1], atom_pos[i][2] + if partial_charges is not False: + cif_text += '{:<7} {} {:>15.9f} {:>15.9f} {:>15.9f} {:>10.5f}\n'.format( + f"{atom_types[i]}{str(i + 1)}_{atom_labels[i]}", + atom_types[i], + u, + v, + w, + partial_charges[i]) + else: + cif_text += '{:<7} {} {:>15.9f} {:>15.9f} {:>15.9f}\n'.format( + f"{atom_types[i]}{str(i + 1)}_{atom_labels[i]}", + atom_types[i], + u, + v, + w) + + # Write cif_text to file + cif_file = open(os.path.join(path, file_name + '.cif'), 'w') + cif_file.write(cif_text) + cif_file.close() + + +def convert_json_2_cif(origin_path, file_name, destiny_path, charge_type='None'): + """ + Convert a file in format `.json` to `.cif`. + + Parameters + ---------- + origin_path : str + Path to the '.json' file. + file_name : str + Name of the file. Does not neet to contain the `.json` extention. + destiny_path : str + path where the `.cif` file will be saved. + """ + + framework_JSON = read_json(origin_path, file_name) + + cell = framework_JSON['geometry']['cell_matrix'] + atom_labels = framework_JSON['geometry']['atom_labels'] + atom_pos = framework_JSON['geometry']['atom_pos'] + + if charge_type + '_charges' in list(framework_JSON['system'].keys()): + partial_charges = framework_JSON['geometry'][charge_type + '_charges'] + else: + partial_charges = False + + save_cif(destiny_path, + file_name, + cell, + atom_labels, + atom_pos, + partial_charges, + frac_coords=False) + + +def convert_gjf_2_xyz(path, file_name): + + file_name = file_name.split('.')[0] + + atom_labels, atom_pos = read_gjf(path, file_name + '.gjf') + + save_xyz(path, file_name + '.xyz', atom_labels, atom_pos) + + +def convert_xyz_2_gjf(path, file_name): + + file_name = file_name.split('.')[0] + + atom_labels, atom_pos = read_xyz(path, file_name + '.xyz') + + save_xyz(path, file_name + '.gjf', atom_labels, atom_pos) + + +def convert_cif_2_xyz(path, file_name, supercell=[1, 1, 1]): + + file_name = file_name.split('.')[0] + + structure = CifParser(os.path.join(path, file_name + '.cif')).get_structures(primitive=True)[0] + + structure.make_supercell([[supercell[0], 0, 0], [0, supercell[1], 0], [0, 0, supercell[2]]]) + + dict_sctructure = structure.as_dict() + + a, b, c = dict_sctructure['lattice']['a'] + b = dict_sctructure['lattice']['b'] + c = dict_sctructure['lattice']['c'] + + alpha = round(dict_sctructure['lattice']['alpha']) + beta = round(dict_sctructure['lattice']['beta']) + gamma = round(dict_sctructure['lattice']['gamma']) + + atom_labels = [i['label'] for i in dict_sctructure['sites']] + + atom_pos = [i['xyz'] for i in dict_sctructure['sites']] + + temp_file = open(os.path.join(path, file_name + '.xyz'), 'w') + temp_file.write(f'{len(atom_labels)} \n') + + temp_file.write(f'{a} {b} {c} {alpha} {beta} {gamma}\n') + + for i in range(len(atom_labels)): + temp_file.write('{:<5s}{:>15.7f}{:>15.7f}{:>15.7f}\n'.format(atom_labels[i], + atom_pos[i][0], + atom_pos[i][1], + atom_pos[i][2])) + + temp_file.close() + + +def write_json(path, name, COF_json): + + name = name.split('.')[0] + + if os.path.exists(path) is not True: + os.mkdir(path) + + save_path = os.path.join(path, name + '.cjson') + + with open(save_path, 'w', encoding='utf-8') as f: + simplejson.dump(COF_json, + f, + ensure_ascii=False, + separators=(',', ':'), + indent=2, + ignore_nan=True) + + +def read_json(path, name): + + cof_path = os.path.join(path, name + '.json') + + with open(cof_path, 'r') as r: + json_object = simplejson.loads(r.read()) + + return json_object + + +def create_COF_json(name) -> dict: + """ + Create a empty dictionary with the COF information. + """ + + system_info = 'Informations about the system.' + geometry_info = 'Informations about the geometry.' + optimization_info = 'Information about the optimization process.' + adsorption_info = 'Information about the adsorption simulation experiments on RASPA2' + textural_info = 'Information about the textural properties' + spectrum_info = 'Information about spectra simulation.' + experimental_info = 'Experimental data DRX, FTIR, ssNMR, UV-VIS...' + + COF_json = {'system': {'description': system_info, + 'name': name, + 'geo_opt': True, + 'execution_times_seconds': {}}, + 'geometry': {'description': geometry_info}, + 'optimization': {'description': optimization_info}, + 'adsorption': {'description': adsorption_info}, + 'textural': {'description': textural_info}, + 'spectrum': {'description': spectrum_info}, + 'experimental': {'description': experimental_info} + } + + return COF_json + + +def create_empty_CJSON() -> dict: + """ + Create a dictionary with the structure information to be saved using the + chemical JSON format. + """ + + chemJSON = { + "chemicalJson": 1, + "name": "", + "formula": "", + "unitCell": { + "a": 0.0, + "b": 0.0, + "c": 0.0, + "alpha": 0.0, + "beta": 0.0, + "gamma": 0.0, + "cellVectors": [] + }, + "atoms": { + "elements": { + "number": [], + "type": [] + }, + "coords": { + "3d": [], + "3dFractional": [] + }, + "formalCharges": [], + "labels": [] + }, + "bonds": { + "connections": { + "index": [] + }, + "order": [] + }, + "PartialCharges": {}, + "properties": { + "molecularMass": 0, + "totalCharge": 0, + "spinMultiplicity": 1, + "totalEnergy": 0, + "bandGap": 0, + }, + "spectra": {}, + "vibrations": {}, + "metadata": {}, + } + + return chemJSON + + +def create_structure_CJSON(StructureName: str, + CellParameters: list = None, + CellMatrix: list = None, + AtomTypes: list = None, + AtomPositions: list = None, + CartesianPositions: bool = False, + AtomLabels: list = [], + Bonds: list = [], + BondOrders: list = [], + PartialCharges: dict = {}, + ) -> dict: + """ + Creates a dictionary with the structure information to be saved using the + chemical JSON format. + + Parameters + ---------- + StructureName : str + Name of the structure. + CellParameters : list + List with the cell parameters. + CellMatrix : list + List with the cell matrix. Optional + AtomLabels : list + List with the atom labels. + AtomPositions : list + List with the atom positions. + + """ + + chemJSON = create_empty_CJSON() + + chemJSON['name'] = StructureName + chemJSON['formula'] = formula_from_atom_list(AtomTypes) + + if CellParameters is not None: + CellMatrix = cellpar_to_cell(CellParameters) + else: + CellParameters = cell_to_cellpar(CellMatrix) + CellMatrix = np.array(CellMatrix) + + chemJSON['unitCell']['a'] = CellParameters[0] + chemJSON['unitCell']['b'] = CellParameters[1] + chemJSON['unitCell']['c'] = CellParameters[2] + chemJSON['unitCell']['alpha'] = CellParameters[3] + chemJSON['unitCell']['beta'] = CellParameters[4] + chemJSON['unitCell']['gamma'] = CellParameters[5] + chemJSON['unitCell']['cellVectors'] = CellMatrix.flatten().tolist() + + AtomNumbers = [elements_dict(property="atomic_number")[i] for i in AtomTypes] + chemJSON['atoms']['elements']['number'] = AtomNumbers + chemJSON['atoms']['elements']['type'] = AtomTypes + + chemJSON['atoms']['elements']['labels'] = AtomLabels + + if CartesianPositions: + chemJSON['atoms']['coords']['3d'] = np.array(AtomPositions).flatten().tolist() + V_frac = get_cartesian_to_fractional_matrix(*CellParameters) + FracPosition = np.array([np.dot(V_frac, atom) for atom in AtomPositions]).flatten().tolist() + chemJSON['atoms']['coords']['3dFractional'] = FracPosition + + else: + chemJSON['atoms']['coords']['3dFractional'] = np.array(AtomPositions).flatten().tolist() + V_cart = get_fractional_to_cartesian_matrix(*CellParameters) + CartPosition = np.array([np.dot(V_cart, atom) for atom in AtomPositions]).flatten().tolist() + chemJSON['atoms']['coords']['3d'] = CartPosition + + chemJSON['atoms']['PartialCharges'] = PartialCharges + + chemJSON['bonds']['connections']['index'] = Bonds + chemJSON['bonds']['order'] = BondOrders + + return chemJSON + + +def generate_mol_dict(path, file_name, name, code, smiles): + + xsmiles, xsmiles_label, composition = smiles_to_xsmiles(smiles) + + if file_name.endswith('gjf'): + atom_types, atom_pos = read_gjf(path, file_name) + elif file_name.endswith('xyz'): + atom_types, atom_pos = read_xyz(path, file_name) + + mol_dict = { + "name": name, + "smiles": smiles, + "code": code, + "xsmiles": xsmiles, + "xsmiles_label": xsmiles_label, + "formula": composition, + "atoms": { + "elements": {"elementType": atom_types}, + "coords": {"3d": atom_pos.tolist()} + } + } + + print(mol_dict) + + write_json(path, file_name.split('.')[0], mol_dict) diff --git a/build/lib/logger.py b/build/lib/logger.py new file mode 100644 index 00000000..7a67aa74 --- /dev/null +++ b/build/lib/logger.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# Created by Felipe Lopes de Oliveira +# Distributed under the terms of the MIT License. + +""" +The logger creation function. +""" + +import logging.config +import logging.handlers + + +def create_logger(level: str = "DEBUG", + format: str = "detailed", + save_to_file: bool = False, + log_filename: str = 'pycofbuilder.log'): + """ + Build a logger with the given level and format. + + Parameters + ---------- + level : str + The logging level. Default is "DEBUG". + Can be one of "DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL". + format : str + The logging format. Default is "detailed". + Can be one of "simple", "detailed". + save_to_file : bool + Whether to save the logs to a file. Default is False. + log_filename : str + The file to save the logs to. Default is "pycofbuilder.log". + + Returns + ------- + logger : logging.Logger + The logger object. + """ + + # Check if the parameters are valid + allowed_levels = ["DEBUG", "INFO", "WARNING", "ERROR", "CRITICAL"] + assert level.upper() in allowed_levels, "Invalid level, must be one of {}".format(allowed_levels) + + allowed_formats = ["simple", "detailed"] + assert format.lower() in allowed_formats, "Invalid format, must be one of {}".format(allowed_formats) + + # Set up logging + config_log = { + "version": 1, + "disable_existing_loggers": False, + "formatters": { + "simple": { + "format": "%(message)s" + }, + "detailed": { + "format": "[%(levelname)s|%(module)s|L%(lineno)d] %(asctime)s: %(message)s", + "datefmt": "%Y-%m-%dT%H:%M:%S%z" + } + }, + "handlers": { + "stderr": { + "class": "logging.StreamHandler", + "level": level.upper(), + "formatter": format.lower(), + "stream": "ext://sys.stderr" + }, + }, + "loggers": { + "root": {"level": level.upper(), "handlers": ["stderr"]} + } + } + + if save_to_file: + config_log["handlers"]["file"] = { + "class": "logging.FileHandler", + "level": level.upper(), + "formatter": format.lower(), + "filename": log_filename, + "mode": "a", + "encoding": "utf-8" + } + config_log["loggers"]["root"]["handlers"].append("file") + + logger = logging.getLogger("pycofbuilder") + + logging.config.dictConfig(config_log) + + return logger diff --git a/build/lib/tools.py b/build/lib/tools.py new file mode 100644 index 00000000..14f74f17 --- /dev/null +++ b/build/lib/tools.py @@ -0,0 +1,1006 @@ +# -*- coding: utf-8 -*- +# Created by Felipe Lopes de Oliveira +# Distributed under the terms of the MIT License. + +""" +This module contains the tools used by pyCOFBuilder. +""" + +import os +import simplejson +import numpy as np +from scipy.spatial import distance + + +def elements_dict(property='atomic_mass'): + '''Returns a dictionary containing the elements symbol and its selected property. + + Parameters + ---------- + prop : string + The desired property can be: + - "full_name" + - "atomic_number" + - "atomic_mass" + - "polarizability" + - "pauling_electronegativity" + - "thermo_electronegativity" + - "mulliken_electronegativity" + - "sanderson_electronegativity" + - "allen_electronegativity" + - "ghosh_electronegativity" + - "martynov_batsanov_electronegativity" + - "atomic_radius" + - "covalent_radius" + - "vdw_radius" + + Returns + ------- + prop_dic : dictionary + A dictionary containing the elements symbol and its respective property. + ''' + + file_name = os.path.join(os.path.dirname(__file__), 'data', 'periodic_table.json') + + with open(file_name, 'r') as f: + periodic_table = simplejson.load(f) + + prop_list = periodic_table['H'].keys() + + # Check if the property is valid + if property not in prop_list: + raise ValueError('Invalid property. Valid properties are: ' + ', '.join(prop_list)) + + prop_dic = {} + for element in periodic_table: + prop_dic[element] = periodic_table[element][property] + + return prop_dic + + +def unit_vector(vector): + """Return a unit vector in the same direction as x.""" + y = np.array(vector, dtype='float') + norm = y / np.linalg.norm(y) + return norm + + +def angle(v1, v2, unit='degree'): + """ + Calculates the angle between two vectors v1 and v2. + + Parameters + ---------- + v1 : array + (N,1) matrix with N dimensions + v2 : array + (N,1) matrix with N dimensions + unit : str + Unit of the output. Could be 'degree', 'radians' or 'cos'. + + Returns + ------- + angle : float + Angle in the selected unit. + """ + unit_vector1 = unit_vector(v1) + unit_vector2 = unit_vector(v2) + + dot_product = np.dot(unit_vector1, unit_vector2) + + if unit == 'degree': + angle = np.arccos(dot_product) * 180. / np.pi + if unit == 'radians': + angle = np.arccos(dot_product) + if unit == 'cos': + angle = dot_product + return angle + + +def rotation_matrix_from_vectors(vec1, vec2): + ''' + Find the rotation matrix that aligns vec1 to vec2 + + Parameters + ---------- + vec1 : array + (3,3) array + vec2 : array + (3,3) array + Returns + ------- + rotation_matrix : array + A transform matrix (3x3) which when applied to vec1, aligns it with vec2. + ''' + a, b = (vec1 / np.linalg.norm(vec1)).reshape(3), (vec2 / np.linalg.norm(vec2)).reshape(3) + v = np.cross(a, b) + c = np.dot(a, b) + s = np.linalg.norm(v) + if s != 0: + kmat = np.array([[0, -v[2], v[1]], + [v[2], 0, -v[0]], + [-v[1], v[0], 0]]) + + rotation_matrix = np.eye(3) + kmat + kmat.dot(kmat) * ((1 - c) / (s ** 2)) + return rotation_matrix + else: + return np.identity(3) + + +def rmsd(V, W): + """ + Calculate Root-mean-square deviation from two sets of vectors V and W. + Parameters + ---------- + V : array + (N,D) matrix, where N is points and D is dimension. + W : array + (N,D) matrix, where N is points and D is dimension. + Returns + ------- + rmsd : float + Root-mean-square deviation between the two vectors + """ + diff = np.array(V) - np.array(W) + N = len(V) + return np.sqrt((diff * diff).sum() / N) + + +def cell_to_cellpar(cell, radians=False): + """Returns the cell parameters [a, b, c, alpha, beta, gamma] + given a 3x3 cell matrix. + + Angles are in degrees unless radian=True is used. + + Parameters + ---------- + cell : array + (3,3) matrix of cell vectors v1, v2, and v3 + radians : bool + Return the cell angles in radians + + Returns + ------- + cellpar : array + (6,1) vector with the cell parameters + """ + lengths = [np.linalg.norm(v) for v in cell] + angles = [] + for i in range(3): + j = i - 1 + k = i - 2 + ll = lengths[j] * lengths[k] + if ll > 1e-16: + x = np.dot(cell[j], cell[k]) / ll + angle = 180.0 / np.pi * np.arccos(x) + else: + angle = 90.0 + angles.append(angle) + + # Corvet to radians if radians is True + if radians: + angles = [angle * np.pi / 180 for angle in angles] + + return np.array(lengths + angles) + + +def cellpar_to_cell(cellpar, ab_normal=(0, 0, 1), a_direction=None): + """Return a 3x3 cell matrix from cell parameters (a,b,c,alpha,beta, and gamma). + + Angles must be in degrees. + + The returned cell is orientated such that a and b are normal to `ab_normal` and a is + parallel to the projection of `a_direction` in the a-b plane. + + Default `a_direction` is (1,0,0), unless this is parallel to + `ab_normal`, in which case default `a_direction` is (0,0,1). + + The returned cell has the vectors va, vb and vc along the rows. The + cell will be oriented such that va and vb are normal to `ab_normal` + and va will be along the projection of `a_direction` onto the a-b + plane. + + Parameters + ---------- + cellpar : array + (6,1) vector with the cell parameters + ab_normal : array + Normal vector between a and b cell vectors. Default: (0, 0, 1) + a_direction : array + Specific direction for the a vector. Default: None + Returns + ------- + cell : array + (3,3) matrix of cell vectors v1, v2, and v3 + """ + if a_direction is None: + if np.linalg.norm(np.cross(ab_normal, (1, 0, 0))) < 1e-5: + a_direction = (0, 0, 1) + else: + a_direction = (1, 0, 0) + + # Define rotated X,Y,Z-system, with Z along ab_normal and X along the + # projection of a_direction onto the normal plane of Z. + ad = np.array(a_direction) + Z = unit_vector(ab_normal) + X = unit_vector(ad - np.dot(ad, Z) * Z) + Y = np.cross(Z, X) + + # Express va, vb and vc in the X,Y,Z-system + alpha, beta, gamma = 90., 90., 90. + if isinstance(cellpar, (int, float)): + a = b = c = cellpar + elif len(cellpar) == 1: + a = b = c = cellpar[0] + elif len(cellpar) == 3: + a, b, c = cellpar + else: + a, b, c, alpha, beta, gamma = cellpar + + # Handle orthorhombic cells separately to avoid rounding errors + eps = 2 * np.spacing(90.0, dtype=np.float64) # around 1.4e-14 + # alpha + if abs(abs(alpha) - 90) < eps: + cos_alpha = 0.0 + else: + cos_alpha = np.cos(alpha * np.pi / 180.0) + # beta + if abs(abs(beta) - 90) < eps: + cos_beta = 0.0 + else: + cos_beta = np.cos(beta * np.pi / 180.0) + # gamma + if abs(gamma - 90) < eps: + cos_gamma = 0.0 + sin_gamma = 1.0 + elif abs(gamma + 90) < eps: + cos_gamma = 0.0 + sin_gamma = -1.0 + else: + cos_gamma = np.cos(gamma * np.pi / 180.0) + sin_gamma = np.sin(gamma * np.pi / 180.0) + + # Build the cell vectors + va = a * np.array([1, 0, 0]) + vb = b * np.array([cos_gamma, sin_gamma, 0]) + cx = cos_beta + cy = (cos_alpha - cos_beta * cos_gamma) / sin_gamma + cz_sqr = 1. - cx * cx - cy * cy + assert cz_sqr >= 0 + cz = np.sqrt(cz_sqr) + vc = c * np.array([cx, cy, cz]) + + # Convert to the Cartesian x,y,z-system + abc = np.vstack((va, vb, vc)) + T = np.vstack((X, Y, Z)) + cell = np.dot(abc, T) + + return cell + + +def get_fractional_to_cartesian_matrix(cell_a: float, + cell_b: float, + cell_c: float, + alpha: float, + beta: float, + gamma: float, + angle_in_degrees: bool = True) -> np.array(float): + """ + Return the transformation matrix that converts fractional coordinates to + cartesian coordinates. + + Parameters + ---------- + a, b, c : float + The lengths of the edges. + alpha, gamma, beta : float + The angles between the sides. + angle_in_degrees : bool + True if alpha, beta and gamma are expressed in degrees. + Returns + ------- + T_matrix : ndarray + The 3x3 rotation matrix. ``V_cart = np.dot(T_matrix, V_frac)``. + """ + if angle_in_degrees: + alpha = np.deg2rad(alpha) + beta = np.deg2rad(beta) + gamma = np.deg2rad(gamma) + + cosa = np.cos(alpha) + cosb = np.cos(beta) + cosg = np.cos(gamma) + sing = np.sin(gamma) + + volume = np.sqrt(1.0 - cosa**2.0 - cosb**2.0 - cosg**2.0 + 2.0 * cosa * cosb * cosg) + + T_matrix = np.zeros((3, 3)) + + T_matrix[0, 0] = cell_a + T_matrix[0, 1] = cell_b * cosg + T_matrix[0, 2] = cell_c * cosb + T_matrix[1, 1] = cell_b * sing + T_matrix[1, 2] = cell_c * (cosa - cosb * cosg) / sing + T_matrix[2, 2] = cell_c * volume / sing + + return T_matrix + + +def get_cartesian_to_fractional_matrix(a: float, + b: float, + c: float, + alpha: float, + beta: float, + gamma: float, + angle_in_degrees: bool = True) -> np.array(float): + """ + Return the transformation matrix that converts cartesian coordinates to + fractional coordinates. + + Parameters + ---------- + a, b, c : float + The lengths of the edges. + alpha, gamma, beta : float + The angles between the sides. + angle_in_degrees : bool + True if alpha, beta and gamma are expressed in degrees. + + Returns + ------- + T_matrix : np.array + The 3x3 rotation matrix. ``R_frac = np.dot(T_matrix, R_cart)``. + """ + if angle_in_degrees: + alpha = np.deg2rad(alpha) + beta = np.deg2rad(beta) + gamma = np.deg2rad(gamma) + + cosa = np.cos(alpha) + cosb = np.cos(beta) + cosg = np.cos(gamma) + sing = np.sin(gamma) + + volume = np.sqrt(1.0 - cosa**2.0 - cosb**2.0 - cosg**2.0 + 2.0 * cosa * cosb * cosg) + + T_matrix = np.zeros((3, 3)) + + T_matrix[0, 0] = 1.0 / a + T_matrix[0, 1] = -cosg / (a * sing) + T_matrix[0, 2] = (cosa * cosg - cosb) / (a * volume * sing) + T_matrix[1, 1] = 1.0 / (b * sing) + T_matrix[1, 2] = (cosb * cosg - cosa) / (b * volume * sing) + T_matrix[2, 2] = sing / (c * volume) + + return T_matrix + + +def get_reciprocal_vectors(cell) -> tuple: + """ + Get the reciprocal vectors of a cell given in cell parameters of cell vectors. + + Parameters + ---------- + cell : array + (3,1) array for cell vectors or (6,1) array for cell parameters + + Returns + ------- + b1 : array + (3,1) array containing b_1 vector in the reciprocal space + b2 : array + (3,1) array containing b_2 vector in the reciprocal space + b3 : array + (3,1) array containing b_3 vector in the reciprocal space + """ + + if len(cell) == 3: + v1, v2, v3 = cell + if len(cell) == 6: + v1, v2, v3 = cellpar_to_cell(cell) + + vol = np.dot(v1, np.cross(v2, v3)) + + b1 = 2 * np.pi * np.cross(v2, v3) / vol + b2 = 2 * np.pi * np.cross(v3, v1) / vol + b3 = 2 * np.pi * np.cross(v1, v2) / vol + + return b1, b2, b3 + + +def get_kgrid(cell, distance=0.3) -> tuple: + """ + Get the k-points grid in the reciprocal space with a given distance for a + cell given in cell parameters of cell vectors. + + Parameters + ---------- + cell : array + (3,1) array for cell vectors or (6,1) array for cell parameters + distance : float + distance between the points in the reciprocal space + Returns + ------- + kx : int + Number of points in the x direction on reciprocal space + ky : int + Number of points in the y direction on reciprocal space + kz : int + Number of points in the z direction on reciprocal space + """ + + b1, b2, b3 = get_reciprocal_vectors(cell) + + b = np.array([np.linalg.norm(b1), np.linalg.norm(b2), np.linalg.norm(b3)]) + + kx = np.ceil(b[0]/distance).astype(int) + ky = np.ceil(b[1]/distance).astype(int) + kz = np.ceil(b[2]/distance).astype(int) + + return kx, ky, kz + + +def create_CellBox(A, B, C, alpha, beta, gamma): + """Creates the CellBox using the same expression as RASPA.""" + + tempd = (np.cos(alpha) - np.cos(gamma) * np.cos(beta)) / np.sin(gamma) + + ax = A + ay = 0 + az = 0 + bx = B * np.cos(gamma) + by = B * np.sin(gamma) + bz = 0 + cx = C * np.cos(beta) + cy = C * tempd + cz = C * np.sqrt(1 - np.cos(beta) ** 2 - tempd ** 2) + + CellBox = np.array([[ax, ay, az], + [bx, by, bz], + [cx, cy, cz]]) + + return CellBox + + +def calculate_UnitCells(cell, cutoff): + ''' + Calculate the number of unit cell repetitions so that all supercell lengths are larger than + twice the interaction potential cut-off radius. + + RASPA considers the perpendicular directions the directions perpendicular to the `ab`, `bc`, + and `ca` planes. Thus, the directions depend on who the crystallographic vectors `a`, `b`, + and `c` are and the length in the perpendicular directions would be the projections + of the crystallographic vectors on the vectors `a x b`, `b x c`, and `c x a`. + (here `x` means cross product) + + Parameters + ---------- + cell_matrix : array + (3,3) cell vectors or (6,1) + Returns + ------- + superCell + (3,1) list containg the number of repiting units in `x`, `y`, `z` directions. + ''' + + # Make sure that the cell is in the format of cell matrix + if len(cell) == 6: + cell_box = cellpar_to_cell(cell) + if len(cell) == 3: + cell_box = cell + + # Pre-calculate the cross products + axb = np.cross(cell_box[0], cell_box[1]) + bxc = np.cross(cell_box[1], cell_box[2]) + cxa = np.cross(cell_box[2], cell_box[0]) + + # Calculates the cell volume + V = np.dot(np.cross(cell_box[0], cell_box[1]), cell_box[2]) + + # Calculate perpendicular widths + cx = V / np.linalg.norm(bxc) + cy = V / np.linalg.norm(cxa) + cz = V / np.linalg.norm(axb) + + # Calculate UnitCells array + supercell = np.ceil(2.0 * cutoff / np.array([cx, cy, cz])).astype(int) + + return supercell + + +def cellpar_to_lammpsbox(a: float, + b: float, + c: float, + alpha: float, + beta: float, + gamma: float, + angle_in_degrees: bool = True): + """ + Return the box parameters lx, ly, lz, xy, xz, yz for LAMMPS data input. + Parameters + ---------- + a, b, c : float + The lengths of the edges. + alpha, gamma, beta : float + The angles between the sides. + angle_in_degrees : bool + True if alpha, beta and gamma are expressed in degrees. + Returns + ------- + r : array_like + The 1x6 array with the box parameters 'lx', 'ly', 'lz', 'xy', 'xz', 'yz'. + """ + if angle_in_degrees: + alpha = alpha*(np.pi/180) + beta = beta*(np.pi/180) + gamma = gamma*(np.pi/180) + + lx = a + xy = b * np.cos(gamma) + xz = c * np.cos(beta) + ly = np.sqrt(b ** 2 - xy ** 2) + yz = (b * c * np.cos(alpha) - xy * xz) / ly + lz = np.sqrt(c ** 2 - xz ** 2 - yz ** 2) + + return np.array([lx, ly, lz, xy, xz, yz]) + + +def find_index(element, e_list): + """ + Finds the index of a given element in a list + + Parameters + ---------- + element : string + String containing the label of the element in e_list + e_list : list + List with the atom labels + Returns + ---------- + i : int + The index of element in the e_list + """ + + index = None + for i in range(len(e_list)): + if np.array_equal(e_list[i], element): + index = i + break + return index + + +def change_X_atoms(atom_labels, atom_pos, bond_atom) -> tuple: + ''' + Changes the X atom for the desired bond_atom or remove it if bond_atom == 'R'. + + Parameters + ---------- + atom_labels : list + List containing the atom labels + atom_pos : list + List containing the atom position + Returns + ---------- + labels : list + List containing the processed atom labels + pos : list + List containing the processed atom position + ''' + label, pos = [], [] + + for i in range(len(atom_labels)): + if atom_labels[i] == 'X' and bond_atom != 'R': + label += [bond_atom] + pos += [atom_pos[i]] + if atom_labels[i] != 'X': + label += [atom_labels[i]] + pos += [atom_pos[i]] + + return label, pos + + +def closest_atom(label_1: str, pos_1: list, labels: list, pos: list): + ''' + Find the closest atom to a given atom + + Parameters + ---------- + label_1 : string + String containing the label of the atom + pos_1 : list + Array containing the position of the atom + labels : list + List containing the all the atom labels on the structure + pos : list + List containing the all the atom positions on the structure + + Returns + ---------- + closest_label : string + String containing the label of the closest atom + closest_position : array + Array containing the position of the closest atom + euclidian_distance : float + Euclidian distance between the two atoms + ''' + + list_labels = [] + list_pos = [] + + for i in range(len(labels)): + if labels[i] != label_1: + list_labels += [labels[i]] + list_pos += [pos[i]] + + if len(list_pos) == 0: + return None, np.array([0, 0, 0]), None + + closest_index = distance.cdist([pos_1], list_pos).argmin() + + closest_label = list_labels[closest_index] + closest_position = list_pos[closest_index] + euclidian_distance = np.linalg.norm(pos_1 - list_pos[closest_index]) + + return closest_label, closest_position, euclidian_distance + + +def closest_atom_struc(label_1, pos_1, labels, pos): + '''Finds the closest atom on the structure to a given atom''' + + list_labels = [] + list_pos = [] + for i in range(len(labels)): + if labels[i] != label_1: + if 'C' in labels[i]: + list_labels += [labels[i]] + list_pos += [pos[i]] + + closest_index = distance.cdist([pos_1], list_pos).argmin() + + closet_label = list_labels[closest_index] + closet_position = list_pos[closest_index] + euclidian_distance = np.linalg.norm(pos_1 - list_pos[closest_index]) + + return closet_label, closet_position, euclidian_distance + + +def get_bond_atom(connector_1: str, connector_2: str) -> str: + ''' + Get the atom that will be used to bond two building blocks. + ''' + + bond_dict = { + 'NH2': 'N', + 'NHOH': 'N', + 'COCHCHOH': 'N', + 'CONHNH2': 'N', + 'CHNNH2': 'N', + 'COOH': 'N', + 'BOH2': 'B', + 'OH2': 'B', + 'Cl': 'X', + 'Br': 'X', + 'CH2CN': 'C', + 'CH3': 'C' + } + + bond_atom = None + for group in list(bond_dict.keys()): + if group in [connector_1, connector_2]: + bond_atom = bond_dict[group] + + return bond_atom + + +def get_framework_symm_text(name, lattice, hall, space_group, space_number, symm_op): + '''Get the text for the framework symmop''' + text = '{:<60s} {:^12s} {:<4s} {:^4s} #{:^5s} {:^2} sym. op.'.format(name, + lattice, + hall.lstrip('-'), + space_group, + space_number, + symm_op) + return text + + +def print_framework_name(name, lattice, hall, space_group, space_number, symm_op): + '''Print the results of the created structure''' + print('{:<60s} {:^12s} {:<4s} {:^4s} #{:^5s} {:^2} sym. op.'.format(name, + lattice, + hall.lstrip('-'), + space_group, + space_number, + symm_op)) + + +def print_command(text, verbose, match): + if verbose in match: + print(text) + + +def formula_from_atom_list(AtomLabels: list) -> str: + """ + Create a string with the formula of the structure from the list of atoms. + + Parameters + ---------- + AtomLabels : list + List of strings containing the atom labels. + + Returns + ------- + formula : str + String with the formula of the structure. + """ + + formula = '' + for i in set(AtomLabels): + formula += i + str(AtomLabels.count(i)) + + return formula + + +def smiles_to_xsmiles(smiles_string: str) -> str: + ''' + Converts a SMILES string to an extended SMILES string with labels + + Parameters + ---------- + smiles_string : str + SMILES string to be converted + + Returns + ------- + xsmiles : str + Extended SMILES string with special labels + xsmiles_label : str + xsmiles labels for images with the special labels + composition : str + String containing the composition + ''' + SPECIAL_ATOMS = ['Q', 'R', 'X'] + REGULAR_ATOMS = ['C', 'N', 'H', 'O', 'S', 'B'] + + xsmiles = '' + labels = [] + atom_list = [] + + for i, letter in enumerate(smiles_string): + + if letter in SPECIAL_ATOMS: + xsmiles += '*' + labels.append(letter) + if letter == 'R': + atom_list.append(smiles_string[i:i+2]) + else: + atom_list.append(letter) + + elif letter.isnumeric(): + if smiles_string[i-1] == 'R': + labels[-1] = labels[-1] + letter + else: + xsmiles += letter + + elif letter in REGULAR_ATOMS: + xsmiles += letter + labels += [''] + atom_list.append(letter) + + else: + xsmiles += letter + + # Generate the xsmiles label + xsmiles_label = '|$' + ';'.join(labels) + '$|' + + # Generate the composition + composition = formula_from_atom_list(atom_list) + + return xsmiles, xsmiles_label, composition + + +def ibrav_to_cell(ibrav, celldm1, celldm2, celldm3, celldm4, celldm5, celldm6): + """ + Convert a value of ibrav to a cell. + + Parameters + ---------- + ibrav : int + celldmx: float + + Returns + ------- + cell : matrix + The cell as a 3x3 numpy array + """ + + alat = celldm1 * 0.5291772105638411 + + if ibrav == 1: + cell = np.identity(3) * alat + elif ibrav == 2: + cell = np.array([[-1.0, 0.0, 1.0], + [0.0, 1.0, 1.0], + [-1.0, 1.0, 0.0]]) * (alat / 2) + elif ibrav == 3: + cell = np.array([[1.0, 1.0, 1.0], + [-1.0, 1.0, 1.0], + [-1.0, -1.0, 1.0]]) * (alat / 2) + elif ibrav == -3: + cell = np.array([[-1.0, 1.0, 1.0], + [1.0, -1.0, 1.0], + [1.0, 1.0, -1.0]]) * (alat / 2) + elif ibrav == 4: + cell = np.array([[1.0, 0.0, 0.0], + [-0.5, 0.5 * 3**0.5, 0.0], + [0.0, 0.0, celldm3]]) * alat + elif ibrav == 5: + tx = ((1.0 - celldm4) / 2.0)**0.5 + ty = ((1.0 - celldm4) / 6.0)**0.5 + tz = ((1 + 2 * celldm4) / 3.0)**0.5 + cell = np.array([[tx, -ty, tz], + [0, 2 * ty, tz], + [-tx, -ty, tz]]) * alat + elif ibrav == -5: + ty = ((1.0 - celldm4) / 6.0)**0.5 + tz = ((1 + 2 * celldm4) / 3.0)**0.5 + a_prime = alat / 3**0.5 + u = tz - 2 * 2**0.5 * ty + v = tz + 2**0.5 * ty + cell = np.array([[u, v, v], + [v, u, v], + [v, v, u]]) * a_prime + elif ibrav == 6: + cell = np.array([[1.0, 0.0, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.0, celldm3]]) * alat + elif ibrav == 7: + cell = np.array([[1.0, -1.0, celldm3], + [1.0, 1.0, celldm3], + [-1.0, -1.0, celldm3]]) * (alat / 2) + elif ibrav == 8: + cell = np.array([[1.0, 0.0, 0.0], + [0.0, celldm2, 0.0], + [0.0, 0.0, celldm3]]) * alat + elif ibrav == 9: + cell = np.array([[1.0 / 2.0, celldm2 / 2.0, 0.0], + [-1.0 / 2.0, celldm2 / 2.0, 0.0], + [0.0, 0.0, celldm3]]) * alat + elif ibrav == -9: + cell = np.array([[1.0 / 2.0, -celldm2 / 2.0, 0.0], + [1.0 / 2.0, celldm2 / 2.0, 0.0], + [0.0, 0.0, celldm3]]) * alat + elif ibrav == 10: + cell = np.array([[1.0 / 2.0, 0.0, celldm3 / 2.0], + [1.0 / 2.0, celldm2 / 2.0, 0.0], + [0.0, celldm2 / 2.0, celldm3 / 2.0]]) * alat + elif ibrav == 11: + cell = np.array([[1.0 / 2.0, celldm2 / 2.0, celldm3 / 2.0], + [-1.0 / 2.0, celldm2 / 2.0, celldm3 / 2.0], + [-1.0 / 2.0, -celldm2 / 2.0, celldm3 / 2.0]]) * alat + elif ibrav == 12: + sinab = (1.0 - celldm4**2)**0.5 + cell = np.array([[1.0, 0.0, 0.0], + [celldm2 * celldm4, celldm2 * sinab, 0.0], + [0.0, 0.0, celldm3]]) * alat + elif ibrav == -12: + sinac = (1.0 - celldm5**2)**0.5 + cell = np.array([[1.0, 0.0, 0.0], + [0.0, celldm2, 0.0], + [celldm3 * celldm5, 0.0, celldm3 * sinac]]) * alat + elif ibrav == 13: + sinab = (1.0 - celldm4**2)**0.5 + cell = np.array([[1.0 / 2.0, 0.0, -celldm3 / 2.0], + [celldm2 * celldm4, celldm2 * sinab, 0.0], + [1.0 / 2.0, 0.0, celldm3 / 2.0]]) * alat + elif ibrav == 14: + sinab = (1.0 - celldm4**2)**0.5 + v3 = [celldm3 * celldm5, + celldm3 * (celldm6 - celldm5 * celldm4) / sinab, + celldm3 * ((1 + 2 * celldm6 * celldm5 * celldm4 + - celldm6**2 - celldm5**2 - celldm4**2)**0.5) / sinab] + cell = np.array([[1.0, 0.0, 0.0], + [celldm2 * celldm4, celldm2 * sinab, 0.0], + v3]) * alat + else: + raise NotImplementedError('ibrav = {0} is not implemented'.format(ibrav)) + + return cell + + +def equal_value(val1, val2, threshold=1e-3) -> bool: + ''' + Determine if two values are equal based on a given threshold. + ''' + return abs(val1 - val2) <= threshold + + +def classify_unit_cell(cell, thr=1e-3) -> str: + ''' + Determine the bravais lattice based on the cell lattice. + The cell lattice can be the cell parameters as (6,1) array or + the cell vectors as (3x3) array. + + Bravais lattice can be cubic, tetragonal, orthorhombic, hexagonal, + monoclinic, or triclinic. + + Parameters + ---------- + cell : array + Array with the cell vectors or parameters + threshold: float + Numeric threshold for the analysis. Default: 1e-3 + + Returns + ------- + cell_type : string + Bravais lattice. + ''' + + if len(cell) == 3: + a, b, c, alpha, beta, gamma = cell_to_cellpar(cell) + + cell_type = None + + if equal_value(alpha, 90, thr) and equal_value(beta, 90, thr) and equal_value(gamma, 90, thr): + if equal_value(a, b, thr) and equal_value(b, c, thr): + cell_type = "cubic" + if equal_value(a, b, thr) and not equal_value(a, c, thr): + cell_type = "tetragonal" + else: + cell_type = "orthorhombic" + elif equal_value(alpha, 90, thr) and equal_value(beta, 90, thr) and equal_value(gamma, 120, thr): + if equal_value(a, b, thr): + cell_type = "hexagonal" + elif equal_value(alpha, 90, thr) or equal_value(beta, 90, thr) or equal_value(gamma, 90, thr): + if not equal_value(a, b, thr) and not equal_value(b, c, thr) and not equal_value(a, c, thr): + cell_type = "monoclinic" + else: + cell_type = "triclinic" + + return cell_type + + +def cell_to_ibrav(cell): + ''' + Return the ibrav number for a given cell. + ''' + + if len(cell) == 3: + a, b, c, alpha, beta, gamma = cell_to_cellpar(cell) + else: + a, b, c, alpha, beta, gamma = cell + + cell_type = classify_unit_cell(cell) + + if cell_type == 'cubic': + celldm = {'ibrav': 1, + 'celldm(1)': a / 0.5291772105638411} + elif cell_type == 'hexagonal': + celldm = {'ibrav': 4, + 'celldm(1)': a / 0.5291772105638411, + 'celldm(3)': c / a} + elif cell_type == 'tetragonal': + celldm = {'ibrav': 6, + 'celldm(1)': a / 0.5291772105638411, + 'celldm(3)': c / a} + elif cell_type == 'orthorhombic': + celldm = {'ibrav': 8, + 'celldm(1)': a / 0.5291772105638411, + 'celldm(2)': b / a, + 'celldm(3)': c / a} + elif cell_type == 'monoclinic': + celldm = {'ibrav': 12, + 'celldm(1)': a / 0.5291772105638411, + 'celldm(2)': b / a, + 'celldm(3)': c / a, + 'celldm(4)': np.cos(np.deg2rad(beta))} + else: + celldm = {'ibrav': 14, + 'celldm(1)': a / 0.5291772105638411, + 'celldm(2)': b / a, + 'celldm(3)': c / a, + 'celldm(4)': np.cos(np.deg2rad(alpha)), + 'celldm(5)': np.cos(np.deg2rad(beta)), + 'celldm(6)': np.cos(np.deg2rad(gamma))} + + return celldm diff --git a/dist/pycofbuilder-0.0.7-py3-none-any.whl b/dist/pycofbuilder-0.0.7-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..911a9ee3b69c2eb409f7b99cf5eefe8ee17392b8 GIT binary patch literal 43748 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 literal 0 HcmV?d00001 diff --git a/dist/pycofbuilder-0.0.7.tar.gz b/dist/pycofbuilder-0.0.7.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..0521ee81e48109ebc314c84bfab70ab9ba5fdfe5 GIT binary patch literal 44246 zcmV)LK)JskiwFqk#^YrI|8RL@Z)Re3X>4R=axE|}FfKPPbYXG;?7i!D+qlvw*uV7@ zZ1SWCH8xwdEvL5ix?ZcbN#cfyv(eaaYKHTF&;0-0;oiaf z-DWISo(EX|E-M{tpD0$uK(|p|NnM)V!iYG-YD`)rM+N$ z8~T@56RX^>SdChvS+(Afep~N?u@_kb&pI0VH=ZB5rP2p4y!NBW4@Q&ssZXHP$Mmk%8w9Oa92hcl?$0 z*`Hhm(}@*&(IoWy2%~EGqyBI@Kpgn%p?~eO7O*-Qil_uYrxEl3@vK_c!N9-3zaHt< zczQncqpPYl@DcR+bOH|}d`QN$iv4&QgjVDYhb4gE1Ky;s33j9mqCUm}ov`(LeHsQ3N|$oUY68i1Z0S=b;0 zb$D=$z%&gx)@a8#X5ew^X?rsRoeLju5AYGTw*&w0 zog5y$$8PK$y+7%|*DCa~a}rnm*WvL&)!OMC9^-(#=^Xu5E#YuNjUxgC>b*apP;jU% zX-1$3e*fe6AO>jdAMCsXSjSiyyT}VWMsyne-{}9}lK#KddJT%##w(-$f8YB5Z#(Y~ z-y9sDIR5BS^ncs|fd1dy+Yu}z;@`9}UfzWg`mKVMJ& zoAVzd|7H3A;UB-Q!*2Izx%R(Smi(`8wQ8Hn`A=(evti`_quBqvi92v7?)pEy5RE=< ztKrm3@7-&!ZHcr|ir*6=DIHI*uifyrZS7cNxBtn#M59w^?FBdP&>Kyxqwvxl`F-n6 z2%tU(;it%oCeyGF|2(T7deq8x?nd6g;^sDQ;`gURH?-XGI1JqWl~Z~fTzl(dfZn#Q zCX;d0e);m!pIlAPoqllr5@%$HhRJ{#C0~wj_m1A|GL(0wldB+X%O=d&eo5c1d)KZ% zRLcApoot7|{+}av6gbn1@K5JF{H4TJoLcNB{xFv%HC215mQ@aPxfWIqb*YwC z4s>}IR}OJGmRAmKVGC?(rk8AyRl8f%GMmP0d6rrZa%mP@4se;4TMlzc7F-T)xt3fG zb-5N@4mGyymOg|5lJfc=op)^uIWRKd>H-W{0(2NfQ!iQo%uAotl%eg5+$sT@g5v$j*Y^S(_R0Fm?HJ_##QQRN3A)dxLGXFR)q(XlATQgo6(xqc59qe7 zAX1;e_+6>bBL8|k^!{`~1gWn^)9dl=FWYrz(`lxv^?mxTp}ra4LNk}%=$CC!KpLR7 zy;4j3IUV0lV4>o zf4fw$CQ@Io&>}JfE&&jFV`vwNRK~<1YQdPYJL|KH+9Dxns&Kf20ThZz6RJ-6e zXdb2zFp2sMBIptq9xdg=(g>j&Tx|?NZHlL51iC-#44Vv4>muuv6K1!}`8I#z1mWe& z3iguh5j@gKVPx zRKILfIZ$`v1&zZO>+86m!6$wgjBtpZTM+b6feihj0I^isn}#8jfF9#Kp92oC25;G>MB-gYSn&AT z9Zd;mfk6fp8!3*C5Y|77Zlg2Y!%T6p!CLmdcrX_}8OU=WmCtR0Iptcw94Qd2PAiod zRDe^m;63mXm?Js?V-zFs!od#?eN(pYKJ1*l?VTL;KKvVT{o@eT*L#`zqOflR-7(nW zP9Jd`_s{9z{I6+*d%0_mEDc;USD>lTWqGD==ZtTGDAx$WTi8s&7S_5ArWVS~Vc-r( z0cj}Nl6J_1>;lF0tv@1o0Q4!WRL-7piQ=9MSSp}{$<)3@Eq_7N?HpV<>A6u*m~)!|Rl2uKUG#gyDIbZ^V2rxu9g z*6WQ{t+ugI1!@KX&VJ-?-FP2=x?Tr)-P+o!)f%s$Q2n*jdR5cWecSqsya@h+hSUFqDmcJr3zSYjf(e1zZ(LYL;_4v^ zB@!XxRkaT78&A-N6HQanTmzzc%4^QCekCe0%4v`y?kDIP$YY{NOPjc_b} zvd;J*^vEFe>SycfqXs;B?@i$M@dQK|ynt~e0*|iH6ofWm9rEsi?mZA+K8T>gj9~Tn zH!y;Z22NuJoah>Qs)K{L(yAkG!n>T_u_WV$mEx|^ni2bIm2vCQ*cWu=3K|{hrT~U} z!fX!%?-GsHq^(!`J*<!#%@TIk#pppbiX_F zN3f>;)2mQ^sso1uriWn-+~~?GuUD9~zIH#Mp@8i6$Yv*kP0B<8GNPLxw6X|Hn(FLK zli_^_rmRR1wDj!z6SUR|MjjC6b!y_tc-!D_Pr}selV<#q@EYayaf`ox3#zC3_U$G! zEoqUDsn#NdL$GJhP%cp_5TO0u`?YiQ$A{xQm>mJun}qs*Rbh3~u-IuC2;K{_k-G3< z%2PJ6cy6!^N@8jewm?L}o*%ghs&yb0{0V3IiDEf+Vb=g0>=)~eW4&|Y^GQNo9~|p9 zSa8t<$SkTtnV#4*IR=)^Zb*U5?gkVWpZKD6CG(6JH)7r}nsqGPEB%59xZxN7h8T?8 z^XO%xS+6j$hjHfo&z%2# z^XGq?%?*?P^*cZR8{e|i4hICraOLNJ@VU7W=YIeSg!RAK+S)Yde~G|i~eWwe|Y2sc|IRbKsd>P;||F%^t8BSpAKgRy^eb|3q8UJr1|3Utb zZ_H+^R4?mH1>{5x~Rwf7#qm(Ufl$imA*|WA8GW4NfhT0ZzqA!QR2vi}V1nqWg zLb5BYi?ld3k8Ru9q@%W2rbmrrcBYYxm7a!BXl@e&tITF%n`g%)57{uI`{DMTZMQ8u z3i~fbR$$-LlFYrg$E{@Br*;t8Nh(75$Huf~*>8meXW50sRA)J$;-qKUWzy--vdglJ zXEO^7-SeCVNpiI8_n0Ov`z>(*&wlMjo?T_mXM$mJ`!udl%rrTtd2)Ue<=ke<`An5d zFjp>SvRu?``Eb)Eg#&ZHg0pdB|1_W!SG|NqI_V*%Iax7jyW{bcROmoN=j-Vb!YoOt1N ze9e$>QI-?O-UpEw;SYdF+!KjqgTJ3ce=_k#V24XlJcL03dkpi$5vXG0?|4qzLlJDHeK5 zjIZ=~L|i2f4;JguU@>1)Spb@Z*<``Hh)eiN6#>i%p3Ug;894OR#*K1{_(3l&b$HaH z1ryk4$oyZ&NAI}tQb6o?_IkvC|`P59+WgpTo7eBb|gRjVGw!I zgd_->uXK;($(UUS^Az+*e9-K&Q{3`X+$)-n$9spk@1Q{RzTE!JGMkZ4>$zwnYPbolh8$P$Q1u{abau0s{DqR=Z-Ev9)w{4RcKhBOn>D{yTm-fE-Y^D zC`aD#qDoxJ+L(z1m2A6ey?F8Ivm0JUyb9SkLP8Nhx-6tghFvHy-C@| zcd$JG=zO#AXW)-;=K!z!pV7I@!a{bXA_FC3eSY4y&S5eXpzIw0Jqf3tQt|w}mxqw> zT186g44@@ThX_%Wn_?5VEzezfh+B?ru~B%@BTu188@Km%Wj37>x+b1JDOmI|0;8U1 zMZuAQn*?N=ww)SL_5#4Zafgvt5GKI~su9P;6r$_}qN>uqG@fLOxqsN1Z`Lem_08;wz|GPqhPjz)(XH{JEyJIQN#)9rie52ry++YKB8E ziz-;YpcJ8!$NEZ`1B<5rQK}^`vIvQ173v9nJGI|sd#B3IohQwL1&>?EBlhvP^^;|H zT6UpoZ#P+mBm7{0*ep~H%J03x@;l`B!Kh)FvytWhwDA)w7zkdtUEw(Yir1~5TPw*`DNDyyQgO55rM zpSGzC6;M08z$D!)o!2-FZu|kyt(3|f#u6t&L82I_*byii)Zo4C*w`V=B9$l^sbQq# zi`pjP{c%YF;XI^qBw1!|lHRpCvm01ShiXwYvA(KFs8~~740IEwid7PTp*NX^tf@Fh zT+fZLG@x6SAYc~1Oc1iAxO{hCwUU1o+o3u{S>jv8B)&$k;B4%H04NW85RinU2}6ZD zC3To=+wRbja-objCWZLUPTs@O6thr49b>HGVCAHNq*0k1suraMqPq}UP*8C(?WB$@ zDdmc-dtWB7=cIZ;49_8%^valW`E6bxI>P1(32p5H9kOa1pCt#wmM!qC4Q!Stp3ANu`;5ro4%NkJ$`;0K>?XM)=Z;n=C7$ z%ES5WT}PLAG9~A&jAfTUx`FW={P>^Inx@{*P!SWqwh1H0w5j|P%Ogp@s%anFclT>C z$4|PrAfugSz)8k!RSig!v6@Dd8BUfE2IhqmytJfzx*)l9E7fF9^(py z=l4B3dlSN{!IIVPIQZ*&H*=4{i!_oVJav3e+Ews^&34!N8Tjv8yKB9$%CD_;!W)Ix zDKiSQo>2W1X5!Z+ca&Lmb3R$LfjY48kjf7pg6Xygb#d+pn zWY;dtE?&H#QAx689yU!b5Kx9$KmqN{{PPEL*MNMX0H6T+0<%gb)y@*(%PFMyg@3r^ zA4tmoq#Z#dK||$ijaSs;63IuUNFx8_aomdwk{;lF1RxJRE-)^=011mET1J>&D2WeZ zEeDi6Oc*j6JMs6jq^(FafS4frMr33f_|lsJFC}8NfQd~m7jp6?F4ez=i4fnQU(q_^ z05lo8mtI+fK}5Sy^;WK0&#PLOtNtxlZRAyLbhYLyst-!Tb&CC|64c1=zlBB_{m(2P z6O%OT>9zQ)N2&j#MT^ketvDq~^pYrd%F2_c>m$=otJX)>xzlbH{*@GqFV!GJ%=ul` zE$2#l|H+w9QtH(2GQGB~Dt9Qi)EH|89p=cl8=kAlEcx6GM43whdq7pJ{yiM8UzttE!vXA{Hkai4#e zXStL!#o4p#Us&l${arJuzl#$(V=jM>=TSd}Y)F4kPv7st^zqT`%r%;#jN!xD(GII# zT=4T-+_vL>Hol931Bpv^k_~SM_OPd2ovgPz^_+K&oOjKfcdeXv8#(VbbKY&`ynB`N z?lpOLFIo5|7?Td-@P-|0cdSp|Z6r}BJizIogXrMvvO<~~IP?mb8T@P}D@zePw65o3 z6uVP+-`jW@EWSs)q74=%bU;O!joqxJYFegw5Fik38K|D>A=1Rpn_TI(Z=;4W3f zTnS(J&L_eeVO+_AF;K!hmyBi58e!0f2<-#^zJ9-7@ zxWl|9(5jW=KS4#}>8MMlGhY=N1QS^Yt;z}yZ&Fm6N!y@NhbTN5-Ng7{9}@~f@}U$2 zi*@L=t&3sc5;}6=IlW#dK9NtW-AT_$ld|ef+m9)W4DOtCC1I_ju*9oHoI27Mi*+WW zJSpOjSRa$R>`I|ow8ZL1taAw`B|UJoxL9Nwz?AC<;n-F><3jfd_?$4+%r!4}(>TZ`11W0i^8H%b+% zSfaFOwq<1$nW{Ua3#cE$UY@Nuq;vXdUEIAen6ws)!>EyyB=)FIAEhNy(FIT8R0+pM zrmZx-q_wXdn?}3v(8wzOw>UybxzUw>F=1Shd}cUM?vlaZjBE0U@*$?AH7?UAtnPJjb*vJ6eYM~va|_=*i^DHy zn}b2GAKykrZ*pX7CAM&D4sr$qYQaohy>^b5^!oj)_`WZG4I9oQUNO$EVN^V0B{J{F zS7vR;?v;Yp6~Cr(ka%O3(|&%jw^-FmoM$R(zp|W_gl|shf#~XcW&8B^u07ApU984r*p z;E)6SN>-(KyyCCCg0dmb8Y$+}Om?=Y^ZcGYWU&jhIA)?xblLuWE;37xP`x`Rhg2u1 zv>^LI<93lDi-(5}d3+OIDWhK^OGuNKI{Q^+Y9Bs}d1&&v6mfjWbY}FNp3Kh)C^a$* zSI?N-y(HLyX*M$`|EA7|Cn5j!ED|o>7{^m?t05{$sd=ifBMb62d&pDN^9JusBv{4;;GDP)L zk>Y=-VUtG6rF=?AlTi|%QI3c7wB~?vPyXR~xI#>67`SuYt||T2`2OUesg}+~G>dO4 zEG-LB>2W63Ge=h5&wM3(f8rJ$>X{1rsi8aYbRU`CTom|T-BXtR`;rULWUvs zu7ewoDOMC68p$;S4z+v zHR-uL>B|*>8r2E}o-FP%q+L848L;F70Eq>xzpeqh70(ltS+Le6XsF&MhXv%ohaCiD z?4On(WYcQ~O`$3CAq<4faZc;)?3JW|U}jMAR33qLH`lMcusBQOR*^}32Zxc2gMUrg zZA_qKgn&pvG#$4r?gS|~m{1;eD$b*LW=sln<@JPzya0@xD8c_ajVW0$^%ND=8%Q{m z^6K3pB#{GbQzgPqosu5>%_O)^{dtoa_VLk(>zE#G=T5lb(c5X{PIzO!2~PuCr_iJ+ zZ@`qV!|&_~Z#?q}Z&Wg>u3*AXuKaMud^d&pZsyK+bH4d*7Fgg-w!oV?^WA*r^WA)u z1|MRqcDd&v(97$EH)hL&WW613ccyO&P!qm+*3D5X=!-4NQh<0 zX7)~EUg#{%zCPa==&bLlLXtT?hnfW&T1T{@;RB{Gr97arYt1r|ukLI}@_yC4!;6-8UShpmC;`5r$V&^pis0W?Zpoor-Y6PHCcxFZ* zZ@zlM4P4u^vkH+AYkC9f?!o*2o>`jiqPFe5w?{L|cIw;q+gW8A1RvioBQfgQL$%PG=RA$1 z(&8;wstFZkZbnH9x~*egnl(~MyG>MC7AdLQC@K-^V*WSU=^(tG4&8@lK3OOUNPJS3 z>Sr~xL+N}W#kZ?82e5g?Fk){7Oc}Gze!%R&Ib8Sa+Gzp$ZwK)tn*r=sU%B8 z>{cyA2Wy-kMdUSsqY{1?N;fM)SsFcVVG_H3Ltja4MYq1U*S84K+^nJ~GqZ1Tz~>RJ-2%Vd9w~;%gt* z+NBu=22xZ+MZ_2N6Nawnc&m#8W}lK3LqemgGst|Qp8c57GeKobX*;}fMPCaIPdZ&Q zy9VuOmRhBuXGx+YUuMU6q?1x;goE%+2=>7AB$U?TY+KtTMVL67FvvpMG2q-JhnUQF zS$brWscSoxaZs;c#QD6o1*T!y4ua39k_^jsF*_I^o9(RdHKgh!(SOsF|$@AnG#eoyJWnjTF`1*gfu21t>#H?8tnb+P2Bg^*-sh3+UcS$llu( zrALeg`0QjX{fgR4g^cIwsg-P{(F=g|SHn7-nO($_#9s~#Sdqw-o>}0d*oe4 zAE!beN%x3K;7}!EXTpY@8oY!oDJ98PWRaVlV!Z=Lojhjqg5_v#r2>WHqe@x>`wDsu z&)A}7V!_&EID{f9ARKzvD%Y!<6;fFM%$#SkfU?&a0GrjO1TcNxq6N&0SxK>s21arN z7op3P<@^?sa&tGqj*)1pV=Zu6!r~<|!PYfksU0;ND1AQ{NM=gShRF&P1Io_xG!)9; z=!rBrnuMxk<%)HMw;9!eBz%}vdTUY&#t!+46N+i3gV6*y6YTtn;|8s1@tO=ib!Z4i zy2kM5;ERh)3U*5N{{F!pCZw!6m~75z;IH~d7jNPD-TRBascq1w12*mStV@_5C$!4iiu$*Yi70gB;`(Q8eGPFQmh_0qm}=R9&1t`X=aN}FF$rwnIp5M z;bKqacoRk&Jp(|a-IJ9mS_9e5oeubEt=s;k-oW&4*8lszld|WTW$UT3{minBRN4RU z|I94hObY)${|7Dne`OYKr3?SxnT0o`!lk`LDQuT^pz56}YG6RbG0_(g215Hj>3nHd z+em#@BlQ`L)TKuDw2d@oHPV>TNJDBw%rhR7szwGEC)c&Tv?|>eujvt@Vni2hB?qF; zPfFu}k!l^E-nE<1^Y$;Voedl#-A9!hiOYA%i4mpX57Lt*r$*E?P+xTejqXa|YZ#|i z)0=}F-)Qz-3(%tu4MUJmi6FK4I+Mu|TzR}nFVVR9y@T-uxp7o$rd51l z2PI*}6n&n4oPL{y58oZkflRWYV4q|(6nV?j2gy%op*73?lY?G*BdI(QT!P4Q zB(Z~OW*pkVEAosvMrevbvO4)Hpf!|7J6OB zo*rV`YS@fuQ>1>1rx!NsR|;{bA52FR;@@1kr|w!~&N_r} z(uWGTG}UOGhpph$(8~`VIJ@*gd|}p~ZOiAx^(PMX-~PBP0A}|=RmH}f8+SN`g;c5B zmvZU-RA@{`GptA5$5{)HOOQg?T7g@^1rw(`Km(_AYRaz%r?*ZJ_i#dX@nzgO;y^ef zksO8X8p2z{5l$*f0V#Ew&sRES%i8+><;2Z@8F6a&@r}I&~_@( z8TFl$Q0P{0w}vi&jBHGQzfB*Laip|nA8{(_3&s?Cd^)MX>n&TP}aacodxpIZJF!V>Bc$Fl(G9HRz z1Ol%qht{iFwh)e_-8`#T5@Va}hIb0R(T>~w*=i~aThfSGd@Dj=BUF*@63;+_*E=N! z1}TTF1ghTdrUGSCj8|H=n!-8>oonM#wf4(DFBYQeU)^a$Onm-mJe_<^0*4+vCV@k9 zGYLGMR^7WEPj1Ql3wah3NT-BMMS6{2;$?^U>_^Az%cQX8_iwiLNkkEiV=1KvOglW8Ddi0wj?WFI1%{zyru6i z##{Cd-#mn&{6qlJlL!p4Lr7Tfk_R8VlZoH=#FHChv7(l)blw^O(nBB%S{D2Z(XN0P zZBtitd_~v?fnFAy0e%tyND|;C@)N>!2uWdGlHH8hr^+^^2dI5Hx$3!gg=12-<>hr= z48nOfgnlsy{Tv*;$*hi$8-9DlAdm@$mYP9*x%W}QW4;ZI$x*(T09$m0+I|g+E0mpcM`AsrSFX= z)&cngtRf2=$ul?&RpVH!GqlMxHIu6gq@*jWwRa@hKaqs0WbVoD|L#_t6Gc9;?q$35 z=yzIoLg(8Y%Ds^On`spVTw*mb2#IfhhJ84``q`gejK0Wq%R%x9FkloKJ-O!b$+%ls^ zQ;Q8U{EP}MT?NY7Giq#TyC4{A+bPcr=K{3@*br2lNkI6p%opxUrL2izwjTYY#P*-I z<}E2fENJcY(URT|iyOAux?{9-m3!SlP-|r-Ld`)uJ{na#Jx-cdvxe@$i(sb7l_zM> zu{+%yr%+QFSYgyuD-y><0hmokq8Tl&tT>?Q>cuuPUPnRgX721B)!kogR$#$#q$g%fNBL73PmCpar zF!>)ICI7=KDgVP}tG4CTwl?auTFYdCF!>)y{s**Ty?o&M-`d*9$p1jsJk9^mYBpP2 zu>QBUTAM#u8z%q5_rd>7{vVV7$NW5P{yz!E!7#YIT_6uoe*Pc$*xXX`|1=u44U_-p zo6i5ECeQHW>>03i#Jm;cm~n)@#l&~idV{C&>i^hC@ME=6~X#IOj-qsp)ITFYzj}fz3O#M zghowL(lc&#syD=^H@HLZdp$8DJGLPd@dlTv_Zkprl{uAmrFS+rUyx)XRSW;e{37IB z0--1-o>T=Gi0H3yQf^jWHkG>dS2fvG(^XI=_$0@stSOwJ-cIo;deJC2Nm9Y5GEL2A zGEIfSZ_b6guYRy!& zMq^|4l22D_*41ihLi@+B``?22-~1|ahl|1VxdU6GqvoS-q%$V%AAGKU1r7=9g z30w{bZ$5Me$x0KZpf#%j%?zqYy)L2?>?}>X78ygEoT1a;e(CF z%S~uxOKfDbH76^3wE>8|ngyi@Lux8El2>zcrN-+AL1@%zoyNw4Mr6GpoaSssxqrAb zKclQ`mf5CenQiKpne>o_3}B~lcth-HYjQ)~^ z>_V#!0Bx{el0i?UV-z_HOU0&YXo*LtzOc9r;y`={?nWBk92|CjOqGX7tA z{$C!CONx@;GH+RygCTOAS}m$v@E~~ipGzu(Dfrg_VblTFJd}}x)zNw$Dpj}DhMZ} z8}7)eIj=0x0icV)wZpGUNL{T&b0qcZ+ZfEnzBJgE0gTT!Xb+PsOxYC-2ipKsy^HDE ziz2)QZ`-o}wbOZj`2N>6iQtZ`yL%GmgnvT3q$P+rYVc^M3cqchHJ;+IiY)82L#u#{ zqq7RQ@n{PfBr>kZNn+T_;#^b`+S=U{b`pya7{3&pWM8nveF={OyaC;EkVjx*B}Q2n z0j#%P>O~H1k=vcyBnxXEPB3uO;Q&*x0*#$d36cVxiDg%Ku@1|pqV{nK6UwJE7?BI1 z7nsq041wK`J`w4zZFUR0JXxz@3MBmi>A@1FSs&@;Q`r-eGwkmJs6GySaw92HA*R}q@&x>7FWzsn|1H5+RDPrX$)`@e5H{wE3Np(=o|6BGl0$bHz}CzkV+rQ~IryqnKzsLI>J))>4x8Ev-quXmhFT8c!kvqKo z8~V>`9>Ye}BWl_Xd&6!SkNW=j)`?JKbBE3(bVm^?YrI}3U>B_dwjBvsVH2Eek~KG% z3p;2Avn=nA3#x;fjZY;DA+Ckd409T!`L6$dkcnhL#h2zVq0Kg{{b?9tx1mL{;8`-e z8xLqQLR*iF2Jrhy7!27hhp_j>U39=q|JI&6!Y1rdF!JzLjS-p{hVTi*vOVz5r}gsAg}DC- zM8rN6qDVm@7y{YM!Bs&>A$GgViMU4W2sq@D}zfWlp-)&oURsj^tA$^WS0x#aU z!-zs7mS~!xx=pg0Ch3>SE5IZ5A{jvb{1jbF$Uj%fF0wXuR&l zmS(BID_Z%+^@r{`0BG#@*yQ^$>M^~&L$vUwbY9$Q>|0531yplBP|X}r1>gXAC$KUB z2^2E}9Vm$fnJy2|(_O`NOjv}tijV1iy2QMRD{Lh2SGE?FECRYUp5`9s=RIgImLpkq zTWn4w?W*fI#oO`il3F&$p4W`@gfw80}52xX0ib}s+guOAg}wM2be7m z3yCRuX&zR}R^tUD0%h+2C~``rB6L3wA>p-(l++o#YDwmhb}CyqlkMBGST7H~8*jKR zHVQ9#Ppm0}wfYVuCOqK^?6&BRv}9M#?el0thsHKObVfF<0iAWVV}OUa!^ zl)XSyRkbKRo-xx>rkgzmAT_71-b)yBGr3ayoYdM-YAJk9t}4GI^9jI&9y!&G?=k^;+_Q|}zahG7};f~J@ zZ?k|1n#y*@6#lGlSR9OfqS4FRNPm_(qQORBJWAGYnqlbFCf(bizjy2OPlG-i(mxaW zaF4y*YuHL|={OnAa-h*9Crg91M9gSfh9!;J$7NzhgDIB=Yl)cA=%rIUSoVSP>%w&~ zAqaD?T;Hf@>h$KXMMDH;)G6hpn@k$pm0 zs*Z-BS@kdM#IAHFDJ}O^>rPTs;s;zgi63yja9^>dYX}%pdIY%nO1zK2wIXG|lh~_J zBSwW5B&H()NiHFRvOTKIU)b--cH}M|*!%yCq?>JiWp~){yM6hdinpi9u`Y=0%z2G* z_!*ISGX>WAhPA-PR{(QgL8P*)cf7&0?+rMF_Dm4ziw{MB3Oc!sr`wjsPb%u3_u*07 zvhLdTy0dw4uloemxo^%3jY@dhZVLCv$ly^)j~)oV+0FvL)7ApchfMWd>Fyf|=ttohPR})D{JshfF~0 z>+96EoLHE1Kgpyhv*jVrQJ-vfDwUiyao27R$Z)nI^5>Paagp)imo0IK{Z%-ZDVU%7 z-PN`NYy{fSUTCs{X6ldAOk>5((48=}R7u?k!AUC-gW~2e7KHw2BHaKXUC2Z49wFvK zvdd()bv(W#Nkj3*8sVx3*0PT7Wh`{N3TEab+uexV?g5)eZV|I#V*r{MIS^r&$;Os8 zVpG!#b1qqR_S3}MK{zzP(<`SW99(d&9X}Gzol1!dwTLq{n^xJOjXRVGfTmrQiZgL9 zU^WTxK!bGF8zhH5bPVjqzDU1>fbVc9q)>8mqR4VWD0!s zl(0bQS69!Y_Yiu7y^ar+{8-ZTgAk8z*|`GoL{xoqbZ0#mZrv%SQ`@%NUv1l%+O}=m z=G3-r+qR~*d;7lc`tDu#pRDX;XP>NOWuKjMlIMYWfuL#SgvXLX53$#l<`YRQjpBk1 z;E0>g%I_J06II!c;(|gj1p94R1~i&q0TC{r^4ymnz#A_T0~0;-W^op>g-^mZfYjXi z2OuR=6L7Wn&gS5c?EOAtw|HybRvzuvKobLWJYzX(md<&tF50QAZKsL z`yeLwMt+-0l23e_Qn~aH3{PN*ss|zo9}2U3&3v;;DfX(NOkf>~XX8k|v8DEpIlth^ z2f7AV{SI^|$O0mRDb5X;ehLOdML=9!@$86Wj^% z%t=vP1SCaNo>G08ZlZoDQ!#%lDm!Zy1-3j}p~x1whMBIO+%FybSxjovQ)@%FiOYLIIfiJX_g z=WrSY72BVqI+nj7o7qz^M?fVn>>d@`uLTn+6*53dhOl!9t7H7;F=}Gvr!}9p0@WH4 z3h7$jLTOFi)UN$OVu$z8M{ghyDEP`3`nurou-=Ir5xs}WK%I+Bz^b!PaeDoX-NpD6 z)8G)}h}la{bI7|jpM*L3R>bp#<+q_yI6K4FvB(3aPtTgo#5-OP#R%LM;kC`CJoL9= zSS^S#DSh&L)5@pAq>fj?5>blHX%C!+3a9Q}QzU@T1|1i9uLcqF zFd$pZ-2n9v+yyA(qCP+GB#L(oUM!#QOO&aS(=T89?gmd_(0sG`}hHa1tg=_9ql|nfYO9}OhKIQvlBwihO@NN$P27xoAKIQh$1%o z>^||al%TP|59M5Gdd@%3ya>73J3+IPi^UX`4&qtFt#4y!)(e|vf{tdd(l`Y~_kMTy zX!DLFYPN$G*^Pfis9?y&M&|PgeKW1IrJj6scEwTWF*>3nAJN zH0w5{^qPpXNL!nj=ww7>?8s=IFGEO4weDGkFmqkhhf%=;(I)je!89hZ>&Y@zPfKa@sf2bxFgBnA8a zZYE?LG+&*Qrv?+}6}wB27tf04cos%*+^bRg;?jn6jyMb z$0-;-j}HD7$LT|EN-8V1bDp~_mENZoo|ae*LWkU2Ank)w(&iH7Y2jMj~!Yzk9|T$Bhm2z5VM zjt{nDeCF=JZs0+F<0$#i9oRv5dW4W5md@*4(}I3h`760RDw0O zV301exe*M!NXV>q`Kn5_@$+abfAob`gzZ~~>brnm6;M;__#&AT!r2B7#7U~jSgE^# zdtn!V0f_Pe{CXt=tvUue>+E$i6CVj@vt{Y& z&9yeKB1=l;Mv$V!|73oxI1V^7V5z5MZ=O-C$5EDUO;B!bt zrqEA`4yan&=^)h2yAsa}@7{HLaet}>k9`)TEBhI&ZjhDOxz7?Yv%<=|_a^6Kb4eO4 z1V(p4%w8mV#x=q%oFsx z0`-nM;d$oPQ4$8U9TM8~q8sy+0{GTN60LCU$2sW`I1%7%O}`D% z(|A&l9474>U{GL1rX$%>1Q&x; zTp$*3tc$blGY!T8QLx=AkoBd7#mrp{kWm_GIM3|6ETi*EFv$C^rh~z<34hhvlF+JZ zDo+OIH}a6qr9-sUn!ae^@oAd*t{f@gv3glK2cE+eC*~@{q;d@P?;yleAbN=hTgw<8 zM|=b2M^dHHg@I{9K{xP|s0uCTo-zKEKb^7CGUrfNac8Wo?r59?Q|Ad&orM+U9m%+{ zO)kL9OjPBiZHgABi6Eq}P8OsMAP*two0kMqU5LY496^a*o{C(KicFP?^ddzC$uLn8 z{}m2Y75Nd3T&L~o?>-b8nPAzzQ-&N2JU9e$?)5k$UuYzz$vwlSqw<)`bx)_o!1EkB z`HDhf_Oly*W>xOT2chE|H%EB7T?QC;yh6O%tXVVxL019q=_r{-nrqVSeCbRO9q0n? zT*(}pP9N1O$GF6L(#8S<434H85*|U#oV%#+|7ispuw?3sRT-6{~E@t^;GKNMJq3+s}5Upl7M8I zq=n<5TV0fUis;a8VL$PnAAP|&pGAR^k~=c8zId!TT)pK!lkqjLRfpvsNucytw7i0^ zrSLj^Wt)Xv7TH+NiKkoKJq`CYH1sLRivm9th5`wtn`NHfioYIAFiof>L08E4EhkE~fUlZ2(GITIm&LS&pIZMS-&6-W%;#||uKaN1%?<%w!^Dmi zQYuYo_>>bxXVKQH;a9%`LNz!4Mqh*A5v)iisQl(5Oj5v<_)1HB^a;ipiKLV=K# z*|9edj$m5_ZLvFfXfD*q30I1=aRQO@BW*gQ$_=Cy#AC^jV|TfrY6 zv0}D?cj00+zeXL(difOb0RZTtjWo1=n?IE0q);~hknk>wBSR_cny(~#_W3qjRzx=2>&X>D5GnW9qwz*jNw z*V$FNAY0Ns?NF{?BA{E+1MOk10E8fk zMnbLifVGHyezA7Rb`IQPOLe~sD1G*n;r)T?w%*|`o-MHS3_b@3vkz4mR#Dt!=nZRb z`i&0dMSoiCwk&1dsJBZ6yKNM^Z7=*whYfXE_w>N*gzs=^q)tEYSk6}1@7NJDaK>S+ zdA0YrA|Km(ijub-$WbOz{0B$Q-Z5q%?USjpG}rr-yZe!Jb8`;cZMfq7}u*LpbV?rTB0uVin~mkEu(358E=fLLC9Vs5Hz z^*vnKVVh1}9|2SBAhOQG*#pee^c}9)Wezws@{XoqE zYMV)8m$`0PsDGFWz_Pan9I5gXEPqiNwlFM@nIUmJG_Qg;41KH^r)ukYO+JxSbF=1m zzxmJdkEQ-atZl)THh60eDG=#OqA#akW#hf=c8g3ct?yxd|`~zuOc1>*3-~ul-<%dRhAC7xTYafoG z_$<|i_-vB!oTT0TP1Ge*{)r;P{a)qeG==}CMq8iO`J{_`5GQOez%hvP{>VaS7mj=a zq1U>~7GQTvD)XtXwL67&0k8{d@A1ptLb=3lCG!euSIcJ3o$#*ZT*#T&yy}2C3U)za zw(&obp6P%}2Y4$FRgZy9X5_l{qrz&?^WTx0ZVB0Avg#AE|EKW%68s+O|MLIVo)1rY zZ~WWhUo%lC%VIx?Sfu{7_OHVKtCOC2mk2oG)q*?fs`~@9iJ2*2Up9R_eBf6O zpJPY*s-8%}J5-Fpzm89eHgLqjO08~6@*kI1U}aaT=2&US{6>J18k2WGP(kx9vvcjM z03lx>8{dga%i!ysT2gV^&mSO-q8~3(`Wr z^t$GD3X~fn&H+>+*6n*voeT>ET}6%p1lJi04fMv$i2_tgTnvJblA}clB;hJf3B*x2 z&4S{)l99xW4>#>!3IsH)Tm*u@@K*CJ?L zJc*?gNQx3D0a~|K3CMGn$C}btU{5@UHdk&DGB_C~^Zc|Bd1r*XHW4I~idRL#Prxx( zobwv}o=B{OEIHlRz*qq(bpec&rS~Z#y(bVK>zHM~<1sOF*$`RE806o1O@qqjGX_DT zvB)=)Za7EGH^S24{M~eOkA$?)t>0lePE1Pw-$ZwZ41hgc5it0T{?xSxO~t7VyVW$a z20|LQ(SMPq?%ur(9?wwob~v~CR}4D@)V|B&CvOMdX2^LXjeRu=yi|9=dR%(;zB>fo z)xU8^plIP2_}t`vg7>|qz^`<>ntrXvoO^pn+xom$sk!nFEw$|?Kwpx*V6qTA`45Tw zFk76w;IWVF4yy;)J(5moW0}~1T-1YHB*9!F{2fvNxj^X&s{!yK(e&)zv$Ml;XWQCUD1x0lciqCHZb zgWj_Dzh=PZgna+g30}lhu~4i0Pk&|WJajU8<^LJ~nF7%K=UE0e|JwLpqyKe)edTj+ z+^|dTZF6(WuDn^1*gG3_lSaYOkMB*m|NI!P&c`|1#~n8 zUM5EIqXhUtjGnY$D{vtr)*VTqyCBy6K(F-rehD<-OhcLGq!0K^{b9c4iWcQb3*l)K za<5B+t0ViK8LEaX=~Ok=Xb|K)ZzsI(?@n~?K$19C`r7EW-?3&*WNaY7-Ps4Oor7%!bO2T?

D@kxi)67HpgK~hjFtQYJ$3J_8RIz6=cqp4a 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 3.10 os numpy >= 1.6.3 scipy >= 1.2 diff --git a/setup.py b/setup.py index 8c93e2fa..dad21e7c 100644 --- a/setup.py +++ b/setup.py @@ -6,7 +6,7 @@ license = f.read() -VERSION = '0.0.6' +VERSION = '0.0.7' DESCRIPTION = 'A package for Covalent Organic Frameworks sturcture assembly.' setup( diff --git a/src/pycofbuilder/pycofbuilder.egg-info/PKG-INFO b/src/pycofbuilder/pycofbuilder.egg-info/PKG-INFO new file mode 100644 index 00000000..787c1d87 --- /dev/null +++ b/src/pycofbuilder/pycofbuilder.egg-info/PKG-INFO @@ -0,0 +1,205 @@ +Metadata-Version: 2.1 +Name: pycofbuilder +Version: 0.0.7 +Summary: A package for Covalent Organic Frameworks sturcture creation based on the reticular approach. +Home-page: https://github.com/lipelopesoliveira/pyCOFBuilder +Author: Felipe Lopes Oliveira +Author-email: Felipe Lopes +License: MIT License + + Copyright (c) 2023, Felipe Lopes de Oliveira + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights + to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + copies of the Software, and to permit persons to whom the Software is + furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice shall be included in all + copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + SOFTWARE. +Project-URL: Homepage, https://github.com/lipelopesoliveira/pyCOFBuilder +Project-URL: Issues, https://github.com/lipelopesoliveira/pyCOFBuilder/issues +Project-URL: Documentation, https://lipelopesoliveira.github.io/pyCOFBuilder/docs/_build/html/index.html +Classifier: Programming Language :: Python +Classifier: Programming Language :: Python :: 3 +Classifier: Programming Language :: Python :: 3.10 +Classifier: Intended Audience :: Science/Research +Classifier: Operating System :: OS Independent +Classifier: Topic :: Scientific/Engineering :: Information Analysis +Classifier: Topic :: Scientific/Engineering :: Physics +Classifier: Topic :: Scientific/Engineering :: Chemistry +Classifier: Topic :: Software Development :: Libraries :: Python Modules +Requires-Python: >=3.10 +Description-Content-Type: text/markdown +License-File: LICENSE +Requires-Dist: os +Requires-Dist: math +Requires-Dist: simplejason +Requires-Dist: numpy>=1.6.3 +Requires-Dist: scipy>=1.2 +Requires-Dist: pymatgen>=2022.0.8 +Requires-Dist: jupyter +Requires-Dist: jupyterlab +Requires-Dist: pandas +Requires-Dist: tqdm +Requires-Dist: gemmi +Requires-Dist: ase + +# pyCOFBuilder + +![puCOFBuilder](docs/img/header.png) + +**pyCOFBuilder** is a simple and powerful python package to automatically assembly COF structures with specifics building blocks, topologies, and functionalizations following the reticular approach to build and represent COF structures. The project was developed to address the need for generation of COFs structures in a high-throughput style, based on a nomenclature tha allows direct sctructural feature interpretation from a simple name. The package uses [pymatgen](https://pymatgen.org/) to create the structures. + +This package is still under development and, but it is already possible to create a large number of COFs structures. + +Learn more at the [Documentation](https://lipelopesoliveira.github.io/pyCOFBuilder/index.html) + +## Requirements + +0. Python >= 3.10 +1. pymatgen >= 2022.0.0 +2. numpy >= 1.2 +3. scipy >= 1.6.3 +4. simplejson +5. ase +6. gemmi + +The Python dependencies are most easily satisfied using a conda +([anaconda](https://www.anaconda.com/distribution)/[miniconda](https://docs.conda.io/en/latest/miniconda.html)) +installation by running + +```Shell +conda env create --file environment.yml +``` + +## Installation + +Currently the best way to use pyCOFBuilder is to manually import it using the `sys` module, as exemplified below: + +```python +# importing module +import sys + +# appending a path +sys.path.append('{PATH_TO_PYCOFBUILDER}/pyCOFBuilder/src') + +import pycofbuilder as pcb +``` + +Just remember to change the `{PATH_TO_PYCOFBUILDER}` to the directory where you download the pyCOFBuilder package. + +## Basic Usage + +To create a specific COF, such as `T3_BENZ_NH2_OH-L2_BENZ_CHO_H-HCB_A-AA`: + +```python +# importing module +import sys + +# appending a path +sys.path.append('{PATH_TO_PYCOFBUILDER}/pyCOFBuilder/src') + +import pycofbuilder as pcb + +cof = pcb.Framework('T3_BENZ_CHO_OH-L2_BENZ_NH2_H-HCB_A-AA') +cof.save(fmt='cif', supercell = [1, 1, 2], save_dir = '.') +``` + +You should see an output such as: + +```python +T3_BENZ_NH2_OH-L2_BENZ_CHO_H_H-HCB_A-AA hexagonal P P6/m # 175 12 sym. op. +``` + +A `.cif` file (the default save format is CIF, but it can be easily changed by setting other value on the `fmt` option) will be created in the `out` folder. The code will print out some information about the structure created. + +Currently, it is possible to select the following formats: + +- `cif` +- `xsf` +- `pdb` +- `cjson` +- `vasp` +- `turbomole` +- `pqr` +- `qe` +- `gjf` +- `xyz` + +Besides, the variable `structure` now is a `Framework` object. This object has some attributes that can be accessed: + +```python +>>> cof.name +'T3_BENZ_NH2_OH-L2_BENZ_CHO_H-HCB_A-AA' +>>> cof.smiles +'(N)C1=C(O)C((N))=C(O)C((N))=C1O.(C([H])=O)C1=C([H])C([H])=C((C([H])=O))C([H])=C1[H]' +>>> cof.lattice +array([[ 22.49540055, 0. , 0. ], + [-11.24770028, 19.48158835, 0. ], + [ 0. , 0. , 3.6 ]]) +>>> cof.n_atoms +72 +>>> cof.space_group +'P6/m' +``` + +## COFs and Building Blocks nomenclature + +In order to ensure greater reproducibility as well as quickly and easily access to relevant information from the COFs, I've developed a simple nomenclature to name the structure. Generally speaking, a COF can be described as + +### `Building_Block_1`-`Building_Block_2`-`Net`-`Stacking` + +where: + +- `Building_Block_1`: The building block with the greater connectivity. +- `Building_Block_2`: The building block with the smaller connectivity. +- `Net`: The net describing the reticular structure. +- `Stacking`: The stacking (for 2D structures) or interpenetrating degree (for 3D structures) + +To name the building blocks I also developed a set of rules. The building block can be described as + +### `Symmetry`\_`Core`\_`Connector`\_`RadicalGroupR1`\_`RadicalGroupR2`\_`RadicalGroupR3`\_`...` + +where: + +- `Symmetry`: The general symmetry of the building block. Also represents the connectivity of the building block. For 2D building blocks can be `L2`, `T3` or `S4`, and for 3D building blocks can be `D4`. +- `Core`: The 4 letters code referring to the building block core. +- `Connector`: The type of functional group that will be used to assembly the COF structure. Ex.: `NH2`, `CHO`, `CONHNH2`, etc. +- `RadicalGroupRN`: The Nth radical group in the structure. The number of Radical groups will change according to the availability of the core. + +Note that every "card" for the building block name is separated by an underline (\_) and every "card" for the COF name is separated by a dash (-). This makes it easy to split the COF name into useful information. + +## Current available Building Blocks + +![Ditopic](docs/img/L2_1.png) +![Ditopic](docs/img/L2_2.png) +![Tritopic](docs/img/T3.png) +![Tetratopic](docs/img/S4.png) +![Hexatopic](docs/img/H6.png) + +## Current available Connector Groups + +![Connection groups](docs/img/Q_GROUPS.png) + +## Current available R Groups + +![Functional Groups](docs/img/R_GROUPS.png) + +## Citation + +If you find **pyCOFBuilder** useful in your research please consider citing the following paper: + +> F. L. Oliveira and P. M. Esteves, +> _pyCOFBuilder: A python package for automated creation of Covalent Organic Framework models based on the reticular approach_ +> +> _arxiv.org/abs/2310.14822_ [DOI](https://doi.org/10.48550/arXiv.2310.14822) diff --git a/src/pycofbuilder/pycofbuilder.egg-info/SOURCES.txt b/src/pycofbuilder/pycofbuilder.egg-info/SOURCES.txt new file mode 100644 index 00000000..e05419be --- /dev/null +++ b/src/pycofbuilder/pycofbuilder.egg-info/SOURCES.txt @@ -0,0 +1,21 @@ +LICENSE +MANIFEST.in +README.md +pyproject.toml +setup.py +src/pycofbuilder/__init__.py +src/pycofbuilder/building_block.py +src/pycofbuilder/cjson.py +src/pycofbuilder/exceptions.py +src/pycofbuilder/framework.py +src/pycofbuilder/io_tools.py +src/pycofbuilder/logger.py +src/pycofbuilder/tools.py +src/pycofbuilder/data/topology.py +src/pycofbuilder/pycofbuilder.egg-info/PKG-INFO +src/pycofbuilder/pycofbuilder.egg-info/SOURCES.txt +src/pycofbuilder/pycofbuilder.egg-info/dependency_links.txt +src/pycofbuilder/pycofbuilder.egg-info/requires.txt +src/pycofbuilder/pycofbuilder.egg-info/top_level.txt +tests/test_advanced.py +tests/test_basic.py \ No newline at end of file diff --git a/src/pycofbuilder/pycofbuilder.egg-info/dependency_links.txt b/src/pycofbuilder/pycofbuilder.egg-info/dependency_links.txt new file mode 100644 index 00000000..8b137891 --- /dev/null +++ b/src/pycofbuilder/pycofbuilder.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/src/pycofbuilder/pycofbuilder.egg-info/requires.txt b/src/pycofbuilder/pycofbuilder.egg-info/requires.txt new file mode 100644 index 00000000..a010dcd8 --- /dev/null +++ b/src/pycofbuilder/pycofbuilder.egg-info/requires.txt @@ -0,0 +1,12 @@ +os +math +simplejason +numpy>=1.6.3 +scipy>=1.2 +pymatgen>=2022.0.8 +jupyter +jupyterlab +pandas +tqdm +gemmi +ase diff --git a/src/pycofbuilder/pycofbuilder.egg-info/top_level.txt b/src/pycofbuilder/pycofbuilder.egg-info/top_level.txt new file mode 100644 index 00000000..01f7e5e9 --- /dev/null +++ b/src/pycofbuilder/pycofbuilder.egg-info/top_level.txt @@ -0,0 +1,9 @@ +__init__ +building_block +cjson +data +exceptions +framework +io_tools +logger +tools