From b48c2333f14976bbbb8e954e31f664e9307e5500 Mon Sep 17 00:00:00 2001 From: John Ferrier <2453747+jpferrierjr@users.noreply.github.com> Date: Sat, 27 Jul 2024 19:15:32 -0400 Subject: [PATCH] Added XYZ animation frames Added functionality for animations of XYZ files when importing with the option "Load all frames" --- source/__init__.py | 6 +- source/xyz_gui.py | 3 +- source/xyz_import.py | 253 ++++++++++++++++++++++++------------------- 3 files changed, 147 insertions(+), 115 deletions(-) diff --git a/source/__init__.py b/source/__init__.py index a46c482..7f6181c 100644 --- a/source/__init__.py +++ b/source/__init__.py @@ -8,12 +8,12 @@ # Start of project : 2011-08-31 by CB # First publication in Blender : 2011-11-11 by CB # Fusion of the PDB, XYZ and Panel : 2019-03-22 by CB -# Last modified : 2024-05-28 by CB +# Last modified : 2024-07-27 by JF # # Contributing authors # ==================== -# -# So far ... none ... . +# John Ferrier (ferrier.j@northeastern.edu) +# # # # Acknowledgements diff --git a/source/xyz_gui.py b/source/xyz_gui.py index e0127ed..8b36d04 100644 --- a/source/xyz_gui.py +++ b/source/xyz_gui.py @@ -27,7 +27,6 @@ from .xyz_export import export_xyz class IMPORT_OT_xyz(Operator, ImportHelper): bl_idname = "import_mesh.xyz" bl_label = "Import XYZ (*.xyz)" - bl_description = "Import a XYZ atomic structure" bl_options = {'PRESET', 'UNDO'} filename_ext = ".xyz" @@ -162,7 +161,7 @@ class IMPORT_OT_xyz(Operator, ImportHelper): self.use_center_all, self.use_camera, self.use_lamp, - filepath_xyz) + filepath_xyz, self.use_frames) # Added self.use_frames for animation purposes in xyz_import.py [ read_xyz_file() ] # Load frames if len(ALL_FRAMES) > 1 and self.use_frames: diff --git a/source/xyz_import.py b/source/xyz_import.py index 816aa6a..34d1e2c 100644 --- a/source/xyz_import.py +++ b/source/xyz_import.py @@ -4,6 +4,7 @@ import os import bpy +import re from math import pi, sqrt from mathutils import Vector, Matrix @@ -192,144 +193,176 @@ def read_elements(): ELEMENTS.append(li) +def extract_number(file_name): + match = re.findall(r'\d+', file_name) + return int(''.join(match)) if match else 0 + + # filepath_pdb: path to pdb file # radiustype : '0' default # '1' atomic radii # '2' van der Waals -def read_xyz_file(filepath_xyz,radiustype): +def read_xyz_file(filepath_xyz,radiustype, use_frames): number_frames = 0 total_number_atoms = 0 - # Open the file ... - filepath_xyz_p = open(filepath_xyz, "r") + #### Added .xyz sequence for animations! Original author has animation functions built in but + # never allowed for ALL_FRAMES to be built out. Just added a functionality for ALL_FRAMES. + # Files need to be in the format of my_cool_file_1.xyz, my_cool_file_2.xyz, ..., my_cool_file_x.xyz + # No other numbers, as extract_number() puts all numbers together for sorting. Guess it could work if + # the pre-emptive numbers are all the same... This was mostly just a quick fix for my use-case though. + # - JF - #Go through the whole file. - FLAG = False - for line in filepath_xyz_p: + # Get the file directory + file_dir = os.path.dirname( filepath_xyz ) + files = [] - # ... the loop is broken here (EOF) ... - if line == "": - continue + # If the use_frames option is selected on import, create a full list of the .xyz files in the dir + if use_frames: + for file in os.listdir( file_dir ): + if file.endswith('.xyz'): + files.append( file ) - split_list = line.rsplit() + # Sort the list of .xyz files + files = sorted( files, key = extract_number ) - if len(split_list) == 1: - number_atoms = int(split_list[0]) - FLAG = True + else: + # If not, just append the single file + files.append( filepath_xyz ) - if FLAG == True: + # Cycle through each sorted file + for xyzf in files: - line = filepath_xyz_p.readline() - line = line.rstrip() + # Open the file ... + filepath_xyz_p = open( os.path.join( file_dir, xyzf ), "r") - all_atoms= [] - for i in range(number_atoms): + #Go through the whole file. + FLAG = False + for line in filepath_xyz_p: + # ... the loop is broken here (EOF) ... + if line == "": + continue - # This is a guarantee that only the total number of atoms of the - # first frame is used. Condition is, so far, that the number of - # atoms in a xyz file is constant. However, sometimes the number - # may increase (or decrease). If it decreases, the addon crashes. - # If it increases, only the tot number of atoms of the first frame - # is used. - # By time, I will allow varying atom numbers ... but this takes - # some time ... - if number_frames != 0: - if i >= total_number_atoms: - break + split_list = line.rsplit() + if len(split_list) == 1: + number_atoms = int(split_list[0]) + FLAG = True + + if FLAG == True: line = filepath_xyz_p.readline() line = line.rstrip() - split_list = line.rsplit() - short_name = str(split_list[0]) - # Go through all elements and find the element of the current atom. - FLAG_FOUND = False - for element in ELEMENTS: - if str.upper(short_name) == str.upper(element.short_name): - # Give the atom its proper name, color and radius: - name = element.name - # int(radiustype) => type of radius: - # pre-defined (0), atomic (1) or van der Waals (2) - radius = float(element.radii[int(radiustype)]) - color = element.color - FLAG_FOUND = True - break - - # Is it a vacancy or an 'unknown atom' ? - if FLAG_FOUND == False: - # Give this atom also a name. If it is an 'X' then it is a - # vacancy. Otherwise ... - if "X" in short_name: - short_name = "VAC" - name = "Vacancy" - radius = float(ELEMENTS[-3].radii[int(radiustype)]) - color = ELEMENTS[-3].color - # ... take what is written in the xyz file. These are somewhat - # unknown atoms. This should never happen, the element list is - # almost complete. However, we do this due to security reasons. - else: - name = str.upper(short_name) - radius = float(ELEMENTS[-2].radii[int(radiustype)]) - color = ELEMENTS[-2].color - - x = float(split_list[1]) - y = float(split_list[2]) - z = float(split_list[3]) - - location = Vector((x,y,z)) - - all_atoms.append([short_name, name, location, radius, color]) - - # We note here all elements. This needs to be done only once. - if number_frames == 0: - - # This is a guarantee that only the total number of atoms of the - # first frame is used. Condition is, so far, that the number of - # atoms in a xyz file is constant. However, sometimes the number - # may increase (or decrease). If it decreases, the addon crashes. - # If it increases, only the tot number of atoms of the first frame - # is used. - # By time, I will allow varying atom numbers ... but this takes - # some time ... - total_number_atoms = number_atoms + all_atoms= [] + for i in range(number_atoms): - elements = [] - for atom in all_atoms: + # This is a guarantee that only the total number of atoms of the + # first frame is used. Condition is, so far, that the number of + # atoms in a xyz file is constant. However, sometimes the number + # may increase (or decrease). If it decreases, the addon crashes. + # If it increases, only the tot number of atoms of the first frame + # is used. + # By time, I will allow varying atom numbers ... but this takes + # some time ... + if number_frames != 0: + if i >= total_number_atoms: + break + + + line = filepath_xyz_p.readline() + line = line.rstrip() + split_list = line.rsplit() + short_name = str(split_list[0]) + + # Go through all elements and find the element of the current atom. FLAG_FOUND = False - for element in elements: - # If the atom name is already in the list, - # FLAG on 'True'. - if element == atom[1]: + for element in ELEMENTS: + if str.upper(short_name) == str.upper(element.short_name): + # Give the atom its proper name, color and radius: + name = element.name + # int(radiustype) => type of radius: + # pre-defined (0), atomic (1) or van der Waals (2) + radius = float(element.radii[int(radiustype)]) + color = element.color FLAG_FOUND = True break - # No name in the current list has been found? => New entry. + + # Is it a vacancy or an 'unknown atom' ? if FLAG_FOUND == False: - # Stored are: Atom label (e.g. 'Na'), the corresponding - # atom name (e.g. 'Sodium') and its color. - elements.append(atom[1]) + # Give this atom also a name. If it is an 'X' then it is a + # vacancy. Otherwise ... + if "X" in short_name: + short_name = "VAC" + name = "Vacancy" + radius = float(ELEMENTS[-3].radii[int(radiustype)]) + color = ELEMENTS[-3].color + # ... take what is written in the xyz file. These are somewhat + # unknown atoms. This should never happen, the element list is + # almost complete. However, we do this due to security reasons. + else: + name = str.upper(short_name) + radius = float(ELEMENTS[-2].radii[int(radiustype)]) + color = ELEMENTS[-2].color - # Sort the atoms: create lists of atoms of one type - structure = [] - for element in elements: - atoms_one_type = [] - for atom in all_atoms: - if atom[1] == element: - atoms_one_type.append(AtomProp(atom[0], - atom[1], - atom[2], - atom[3], - atom[4],[])) - structure.append(atoms_one_type) + x = float(split_list[1]) + y = float(split_list[2]) + z = float(split_list[3]) - ALL_FRAMES.append(structure) - number_frames += 1 - FLAG = False + location = Vector((x,y,z)) - filepath_xyz_p.close() + all_atoms.append([short_name, name, location, radius, color]) + + # We note here all elements. This needs to be done only once. + if number_frames == 0: + + # This is a guarantee that only the total number of atoms of the + # first frame is used. Condition is, so far, that the number of + # atoms in a xyz file is constant. However, sometimes the number + # may increase (or decrease). If it decreases, the addon crashes. + # If it increases, only the tot number of atoms of the first frame + # is used. + # By time, I will allow varying atom numbers ... but this takes + # some time ... + total_number_atoms = number_atoms + + + elements = [] + for atom in all_atoms: + FLAG_FOUND = False + for element in elements: + # If the atom name is already in the list, + # FLAG on 'True'. + if element == atom[1]: + FLAG_FOUND = True + break + # No name in the current list has been found? => New entry. + if FLAG_FOUND == False: + # Stored are: Atom label (e.g. 'Na'), the corresponding + # atom name (e.g. 'Sodium') and its color. + elements.append(atom[1]) + + # Sort the atoms: create lists of atoms of one type + structure = [] + for element in elements: + atoms_one_type = [] + for atom in all_atoms: + if atom[1] == element: + atoms_one_type.append(AtomProp(atom[0], + atom[1], + atom[2], + atom[3], + atom[4],[])) + structure.append(atoms_one_type) + + ALL_FRAMES.append(structure) + number_frames += 1 + FLAG = False + + filepath_xyz_p.close() return total_number_atoms @@ -441,7 +474,7 @@ def import_xyz(Ball_type, put_to_center_all, use_camera, use_light, - filepath_xyz): + filepath_xyz, use_frames): # List of materials atom_material_list = [] @@ -454,7 +487,7 @@ def import_xyz(Ball_type, # ------------------------------------------------------------------------ # READING DATA OF ATOMS - Number_of_total_atoms = read_xyz_file(filepath_xyz, radiustype) + Number_of_total_atoms = read_xyz_file(filepath_xyz, radiustype, use_frames) # We show the atoms of the first frame. first_frame = ALL_FRAMES[0] @@ -540,7 +573,7 @@ def import_xyz(Ball_type, sum_vec = Vector((0.0,0.0,0.0)) # Sum of all atom coordinates - for (i, atoms_of_one_type) in enumerate(frame): + for i, atoms_of_one_type in enumerate(frame): # This is a guarantee that only the total number of atoms of the # first frame is used. Condition is, so far, that the number of -- 2.30.2