diff --git a/aiidalab_widgets_base/structures.py b/aiidalab_widgets_base/structures.py index 48af51852..b90b7a6c0 100644 --- a/aiidalab_widgets_base/structures.py +++ b/aiidalab_widgets_base/structures.py @@ -7,11 +7,11 @@ import pathlib import tempfile from collections import OrderedDict +from copy import deepcopy import ase import ipywidgets as ipw import numpy as np -from copy import deepcopy # spglib for cell converting import spglib @@ -30,7 +30,19 @@ from ase import Atom, Atoms from ase.data import chemical_symbols, covalent_radii from sklearn.decomposition import PCA -from traitlets import Bool,Dict, Instance, Int, List, Unicode, Union, default, dlink, link, observe +from traitlets import ( + Bool, + Dict, + Instance, + Int, + List, + Unicode, + Union, + default, + dlink, + link, + observe, +) # Local imports from .data import LigandSelectorWidget @@ -57,7 +69,7 @@ class StructureManagerWidget(ipw.VBox): structure = Union([Instance(Atoms), Instance(Data)], allow_none=True) structure_node = Instance(Data, allow_none=True, read_only=True) node_class = Unicode() - #brand_new_structure = Bool() + # brand_new_structure = Bool() SUPPORTED_DATA_FORMATS = {"CifData": "cif", "StructureData": "structure"} @@ -161,12 +173,11 @@ def _structure_importers(self, importers): if len(importers) == 1: # Assigning a function which will be called when importer provides a structure. dlink((importers[0], "structure"), (self, "input_structure")) - #if importers[0].has_trait("brand_new_structure"): + # if importers[0].has_trait("brand_new_structure"): # link((importers[0], "brand_new_structure"), (self.viewer, "brand_new_structure")) # link((importers[0], "brand_new_structure"), (self, "brand_new_structure")) return importers[0] - # Otherwise making one tab per importer. importers_tab = ipw.Tab() @@ -175,7 +186,7 @@ def _structure_importers(self, importers): # Labeling tabs. importers_tab.set_title(i, importer.title) dlink((importer, "structure"), (self, "input_structure")) - #if importer.has_trait("brand_new_structure"): + # if importer.has_trait("brand_new_structure"): # link((importer, "brand_new_structure"), (self.viewer, "brand_new_structure")) # link((importer, "brand_new_structure"), (self, "brand_new_structure")) return importers_tab @@ -201,7 +212,7 @@ def _structure_editors(self, editors): editors_tab.set_title(i, editor.title) link((editor, "structure"), (self, "structure")) if editor.has_trait("selection"): - link((editor, "selection"), (self.viewer, "selection")) + link((editor, "selection"), (self.viewer, "selection")) if editor.has_trait("camera_orientation"): dlink( (self.viewer._viewer, "_camera_orientation"), @@ -343,7 +354,6 @@ def _observe_input_structure(self, change): else: self.structure = None - @observe("structure") def _structure_changed(self, change=None): """Perform some operations that depend on the value of `structure` trait. @@ -351,7 +361,7 @@ def _structure_changed(self, change=None): This function enables/disables `btn_store` widget if structure is provided/set to None. Also, the function sets `structure_node` trait to the selected node type. """ - + if not self.structure_set_by_undo: self.history.append(change["new"]) @@ -692,7 +702,7 @@ class SmilesWidget(ipw.VBox): """Convert SMILES into 3D structure.""" structure = Instance(Atoms, allow_none=True) - #brand_new_structure = Bool() + # brand_new_structure = Bool() SPINNER = """""" @@ -1195,8 +1205,7 @@ def disable_element(_=None): ] ) - - def find_index(self,list_of_lists, element): + def find_index(self, list_of_lists, element): for i, x in enumerate(list_of_lists): if element in x: return i @@ -1278,7 +1287,7 @@ def translate_dr(self, _=None, atoms=None, selection=None): self.action_vector * self.displacement.value ) - #self.brand_new_structure = False + # self.brand_new_structure = False self.structure, self.selection = atoms, selection @_register_structure @@ -1289,7 +1298,7 @@ def translate_dxdydz(self, _=None, atoms=None, selection=None): # The action. atoms.positions[self.selection] += np.array(self.str2vec(self.dxyz.value)) - #self.brand_new_structure = False + # self.brand_new_structure = False self.structure, self.selection = atoms, selection @@ -1301,7 +1310,7 @@ def translate_to_xyz(self, _=None, atoms=None, selection=None): geo_center = np.average(self.structure[self.selection].get_positions(), axis=0) atoms.positions[self.selection] += self.str2vec(self.dxyz.value) - geo_center - #self.brand_new_structure = False + # self.brand_new_structure = False self.structure, self.selection = atoms, selection @_register_structure @@ -1316,7 +1325,7 @@ def rotate(self, _=None, atoms=None, selection=None): rotated_subset.rotate(self.phi.value, v=vec, center=center, rotate_cell=False) atoms.positions[list(self.selection)] = rotated_subset.positions - #self.brand_new_structure = False + # self.brand_new_structure = False self.structure, self.selection = atoms, selection @_register_structure @@ -1350,7 +1359,7 @@ def mirror(self, _=None, norm=None, point=None, atoms=None, selection=None): # Mirror atoms. atoms.positions[selection] -= 2 * projections - #self.brand_new_structure = False + # self.brand_new_structure = False self.structure, self.selection = atoms, selection def mirror_3p(self, _=None): @@ -1375,7 +1384,7 @@ def align(self, _=None, atoms=None, selection=None): subset.rotate(self.action_vector, self.str2vec(self.dxyz.value), center=center) atoms.positions[selection] = subset.positions - #self.brand_new_structure = False + # self.brand_new_structure = False self.structure, self.selection = atoms, selection @_register_structure @@ -1384,7 +1393,6 @@ def mod_element(self, _=None, atoms=None, selection=None): """Modify selected atoms into the given element.""" last_atom = atoms.get_global_number_of_atoms() - if self.ligand.value == 0: for idx in self.selection: new = Atom(self.element.value) @@ -1399,17 +1407,15 @@ def mod_element(self, _=None, atoms=None, selection=None): initial_ligand = self.ligand.rotate( align_to=self.action_vector, remove_anchor=True ) - + for idx in self.selection: position = self.structure.positions[idx].copy() lgnd = initial_ligand.copy() lgnd.translate(position) - atoms += lgnd + atoms += lgnd new_selection = [ i for i in range(last_atom, last_atom + len(selection) * len(lgnd)) ] - - # the order of the traitlets below is important self.selection = [] @@ -1428,12 +1434,8 @@ def copy_sel(self, _=None, atoms=None, selection=None): add_atoms.translate([1.0, 0, 0]) atoms += add_atoms - - new_selection = [i for i in range(last_atom, last_atom + len(selection))] - - # the order of the traitlets below is important self.selection = [] self.structure = atoms @@ -1471,7 +1473,7 @@ def add(self, _=None, atoms=None, selection=None): i for i in range(last_atom, last_atom + len(selection) * len(lgnd)) ] - #self.brand_new_structure = False + # self.brand_new_structure = False # the order of the traitlets below is important self.selection = [] @@ -1484,6 +1486,5 @@ def remove(self, _, atoms=None, selection=None): """Remove selected atoms.""" del [atoms[selection]] - self.selection = [] - self.structure = atoms \ No newline at end of file + self.structure = atoms diff --git a/aiidalab_widgets_base/viewers.py b/aiidalab_widgets_base/viewers.py index 26da7f95a..4725e5a6c 100644 --- a/aiidalab_widgets_base/viewers.py +++ b/aiidalab_widgets_base/viewers.py @@ -2,12 +2,12 @@ # pylint: disable=no-self-use import base64 -from hashlib import new +import itertools import re import warnings from copy import deepcopy +from hashlib import new -import itertools import ipywidgets as ipw import nglview import numpy as np @@ -15,7 +15,7 @@ import traitlets from aiida.cmdline.utils.common import get_workchain_report from aiida.cmdline.utils.query import formatting -from aiida.orm import Node, Data +from aiida.orm import Data, Node from ase import Atoms, neighborlist from ase.cell import Cell from IPython.display import clear_output, display @@ -51,7 +51,9 @@ from .utils import ase2spglib, list_to_string_range, string_range_to_list AIIDA_VIEWER_MAPPING = dict() -BOX_LAYOUT = ipw.Layout(display='flex-wrap', flex_flow='row wrap', justify_content='space-between') +BOX_LAYOUT = ipw.Layout( + display="flex-wrap", flex_flow="row wrap", justify_content="space-between" +) def register_viewer_widget(key): @@ -163,31 +165,39 @@ def __init__(self, parameter, downloadable=True, **kwargs): class Representation(ipw.HBox): """Representation for StructureData in nglviewer - if a structure is imported the traitlet import_structure will trigger - initialization of the list 'list_of_representations' e.g. [[0,1,2,3,4,5]] - the traitlet 'list_of_representations' will trigger creation of self.representations as list of Representations() - default style set in self.representations[0].style is 'molecule' - self.representations[0].selection is populated with all atoms - self.update_representations() which copyes selections in the representations widgets into the list_of_representations - ad calls self.apply_representations() - - apply_representations: - creates the representation dictionary for the main structure 'rep_dict_unit', replicates teh reprsentattions in case - of supercell and calls self.update_viewer() that creates teh nglview representattions - - Editors of a structure update the traitlet self.list_of_representations. + if a structure is imported the traitlet import_structure will trigger + initialization of the list 'list_of_representations' e.g. [[0,1,2,3,4,5]] + the traitlet 'list_of_representations' will trigger creation of self.representations as list of Representations() + default style set in self.representations[0].style is 'molecule' + self.representations[0].selection is populated with all atoms + self.update_representations() which copyes selections in the representations widgets into the list_of_representations + ad calls self.apply_representations() + + apply_representations: + creates the representation dictionary for the main structure 'rep_dict_unit', replicates teh reprsentattions in case + of supercell and calls self.update_viewer() that creates teh nglview representattions + + Editors of a structure update the traitlet self.list_of_representations. """ + master_class = None + def __init__(self, indices="1..2"): - self.selection = ipw.Text(description="atoms:",value="",style={"description_width": "initial"} ) - self.style = ipw.Dropdown(options=["molecule","surface"],value="molecule",description="mode",disabled=False) - self.show = ipw.Checkbox(value=True,description="show",disabled=False) + self.selection = ipw.Text( + description="atoms:", value="", style={"description_width": "initial"} + ) + self.style = ipw.Dropdown( + options=["molecule", "surface"], + value="molecule", + description="mode", + disabled=False, + ) + self.show = ipw.Checkbox(value=True, description="show", disabled=False) # Delete button. self.delete_button = ipw.Button(description="Delete", button_style="danger") self.delete_button.on_click(self.delete_myself) - super().__init__( children=[ self.selection, @@ -195,12 +205,12 @@ def __init__(self, indices="1..2"): self.show, self.delete_button, ] - ) + ) - def delete_myself(self, _): self.master_class.delete_representation(self) + class _StructureDataBaseViewer(ipw.VBox): """Base viewer class for AiiDA structure or trajectory objects. @@ -212,9 +222,10 @@ class _StructureDataBaseViewer(ipw.VBox): :type default_camera: string """ + representations = traitlets.List() natoms = Int() - #brand_new_structure = Bool(True) + # brand_new_structure = Bool(True) selection = List(Int) selection_adv = Unicode() supercell = List(Int) @@ -237,28 +248,30 @@ def __init__( self._viewer.camera = default_camera self._viewer.observe(self._on_atom_click, names="picked") self._viewer.stage.set_parameters(mouse_preset="pymol") - #self.first_update_of_viewer = True - self.natoms=0 - self.rep_dict={} - self.rep_dict_unit={} - self.default_representations={ "molecule": { - "ids": "1..2", - "aspectRatio": 3.5, - "highlight_aspectRatio": 3.6, - "highlight_color": "red", - "highlight_opacity": 0.6, - "name": "molecule", - "type": "ball+stick", - }, - "surface": { - "ids": "1..2", - "aspectRatio": 10, - "highlight_aspectRatio": 10.1, - "highlight_color": "green", - "highlight_opacity": 0.6, - "name": "surface", - "type": "ball+stick", - },} + # self.first_update_of_viewer = True + self.natoms = 0 + self.rep_dict = {} + self.rep_dict_unit = {} + self.default_representations = { + "molecule": { + "ids": "1..2", + "aspectRatio": 3.5, + "highlight_aspectRatio": 3.6, + "highlight_color": "red", + "highlight_opacity": 0.6, + "name": "molecule", + "type": "ball+stick", + }, + "surface": { + "ids": "1..2", + "aspectRatio": 10, + "highlight_aspectRatio": 10.1, + "highlight_color": "green", + "highlight_opacity": 0.6, + "name": "surface", + "type": "ball+stick", + }, + } view_box = ipw.VBox([self._viewer]) @@ -397,19 +410,26 @@ def change_camera(change): center_button = ipw.Button(description="Center molecule") center_button.on_click(lambda c: self._viewer.center()) - # 5. representations buttons - self.atoms_not_represented=ipw.Output() + self.atoms_not_represented = ipw.Output() self.add_new_rep_button = ipw.Button(description="Add rep", button_style="info") self.add_new_rep_button.on_click(self.add_representation) apply_rep = ipw.Button(description="Apply rep") - apply_rep.on_click(self.on_click_apply_representations) + apply_rep.on_click(self.on_click_apply_representations) self.representation_output = ipw.Box(layout=BOX_LAYOUT) return ipw.VBox( - [supercell_selector, background_color, camera_type, - self.add_new_rep_button, self.representation_output,self.atoms_not_represented, apply_rep, center_button] + [ + supercell_selector, + background_color, + camera_type, + self.add_new_rep_button, + self.representation_output, + self.atoms_not_represented, + apply_rep, + center_button, + ] ) def add_representation(self, _): @@ -423,14 +443,16 @@ def delete_representation(self, representation): self.representation_add_message.message = f"""Error: Rep. {representation} not found.""" return - self.representations = self.representations[:index] + self.representations[index+1:] + self.representations = ( + self.representations[:index] + self.representations[index + 1 :] + ) del representation self.apply_representations() @observe("representations") def _observe_representations(self, change): """Update the list of representations.""" - if change['new']: + if change["new"]: self.representation_output.children = change["new"] self.representations[-1].master_class = self else: @@ -443,32 +465,40 @@ def update_representations(self, change=None): if number_of_representation_widgets == 0: self.representations = [Representation()] # shoudl not be needed teh check of more reps in array['representations'] than actually defined reps - - representations = self.structure.arrays["representations"] + + representations = self.structure.arrays["representations"] for rep in set(representations): - if rep >=0: # negative values are used for atoms not represented (different from the case of hidden representations) - self.representations[int(rep)].selection.value = list_to_string_range([int(i) for i in np.where(representations == rep )[0]],shift=1) + if ( + rep >= 0 + ): # negative values are used for atoms not represented (different from the case of hidden representations) + self.representations[ + int(rep) + ].selection.value = list_to_string_range( + [int(i) for i in np.where(representations == rep)[0]], shift=1 + ) # empty selection field for unused representations for rep in range(number_of_representation_widgets): - if rep not in set([int(i) for i in representations]): - self.representations[rep].selection.value = "" + if rep not in {int(i) for i in representations}: + self.representations[rep].selection.value = "" self.apply_representations() def replicate_representations(self, change=None): - #copy representations of structure into rep of display structure + # copy representations of structure into rep of display structure nrep = np.prod(self.supercell) self.rep_dict = deepcopy(self.rep_dict_unit) - if nrep > 1: + if nrep > 1: for component in range(len(self.rep_dict_unit.keys())): - ids = string_range_to_list(self.rep_dict_unit[component]["ids"] , shift=0)[0] - new_ids = [i+rep*self.natoms for rep in range(nrep) for i in ids] - self.rep_dict[component]["ids"] = list_to_string_range(new_ids,shift=0) - self._gen_translation_indexes() - - def on_click_apply_representations(self,change=None): + ids = string_range_to_list( + self.rep_dict_unit[component]["ids"], shift=0 + )[0] + new_ids = [i + rep * self.natoms for rep in range(nrep) for i in ids] + self.rep_dict[component]["ids"] = list_to_string_range(new_ids, shift=0) + self._gen_translation_indexes() + + def on_click_apply_representations(self, change=None): """Updates self.displayed_structure.arrays['representations'] according to user defined representations""" - arrayrepresentations=np.zeros(self.natoms) + arrayrepresentations = np.zeros(self.natoms) for irep, rep in enumerate(self.representations): selection = string_range_to_list(rep.selection.value, shift=-1)[0] for index in selection: @@ -476,46 +506,45 @@ def on_click_apply_representations(self,change=None): self.structure.set_array("representations", arrayrepresentations) - self.apply_representations() - + def apply_representations(self, change=None): + # iterate on number of representations + self.rep_dict_unit = {} + current_rep = 0 - - - def apply_representations(self,change=None): - #iterate on number of representations - self.rep_dict_unit={} - current_rep=0 - - - #self.brand_new_structure=False + # self.brand_new_structure=False for rep in self.representations: # in representation dictionary indexes start from 0 so we transform '1..4' in '0..3' idsl = string_range_to_list(rep.selection.value, shift=-1)[0] - ids = list_to_string_range(idsl,shift=0) + ids = list_to_string_range(idsl, shift=0) - self.rep_dict_unit[current_rep] = deepcopy(self.default_representations[rep.style.value]) + self.rep_dict_unit[current_rep] = deepcopy( + self.default_representations[rep.style.value] + ) self.rep_dict_unit[current_rep]["ids"] = ids - self.rep_dict_unit[current_rep]["name"] = "rep"+str(current_rep) + self.rep_dict_unit[current_rep]["name"] = "rep" + str(current_rep) - current_rep+=1 - missing_atoms = set([int(i) for i in np.where(self.structure.arrays["representations"]<0)[0]]) + current_rep += 1 + missing_atoms = { + int(i) for i in np.where(self.structure.arrays["representations"] < 0)[0] + } if missing_atoms: self.atoms_not_represented.clear_output() with self.atoms_not_represented: - print("Atoms excluded from representations: ",list_to_string_range(list(missing_atoms),shift=1)) + print( + "Atoms excluded from representations: ", + list_to_string_range(list(missing_atoms), shift=1), + ) else: self.atoms_not_represented.clear_output() - #print("before calling replicate the rep dict is",self.rep_dict_unit) + # print("before calling replicate the rep dict is",self.rep_dict_unit) self.replicate_representations() self.update_viewer() - #if self.first_update_of_viewer: - #self.first_update_of_viewer = self.orient_z_up() + # if self.first_update_of_viewer: + # self.first_update_of_viewer = self.orient_z_up() self.orient_z_up() - - @observe("cell") def _observe_cell(self, _=None): # only update cell info when it is a 3D structure. @@ -813,20 +842,19 @@ def _gen_translation_indexes(self): and {(0,0):0,(0,1):1,(0,2):2,(1,0):3,(2,0):4,(2,1):5,(2,2):6,(2,3):7,(2,4):8,(2,5):9} """ - self._translate_i_glob_loc = {} - self._translate_i_loc_glob = {} + self._translate_i_loc_glob = {} for component in range(len(self.rep_dict.keys())): comp_i = 0 ids = list( - string_range_to_list(self.rep_dict[component]["ids"], shift=0)[0]) + string_range_to_list(self.rep_dict[component]["ids"], shift=0)[0] + ) for i_g in ids: self._translate_i_glob_loc[i_g] = (component, comp_i) self._translate_i_loc_glob[(component, comp_i)] = i_g comp_i += 1 - def _translate_glob_loc(self, indexes): """From global index to indexes of different components.""" all_comp = [list() for i in range(len(self.rep_dict.keys()))] @@ -845,29 +873,24 @@ def _on_atom_click(self, _=None): if self.rep_dict: component = self._viewer.picked["component"] - - index = self._translate_i_loc_glob[(component, index)] - + + index = self._translate_i_loc_glob[(component, index)] + selection = self.selection.copy() - + if selection: - if index not in selection : + if index not in selection: selection.append(index) else: selection.remove(index) else: selection = [index] - - #selection_unit = [i for i in selection if i < self.natoms] - self.selection = selection - #self.selection = selection_unit - - - return - - + # selection_unit = [i for i in selection if i < self.natoms] + self.selection = selection + # self.selection = selection_unit + return def highlight_atoms( self, @@ -922,23 +945,26 @@ def highlight_atoms( opacity=opacity, component=component, ) + def update_viewer(self, c=None): with self.hold_trait_notifications(): - + while hasattr(self._viewer, "component_0"): self._viewer.component_0.clear_representations() cid = self._viewer.component_0.id self._viewer.remove_component(cid) - #copy representations of structure into rep of display structure + # copy representations of structure into rep of display structure if self.displayed_structure: - #print("inside displaying") + # print("inside displaying") for component in range(len(self.rep_dict)): rep_indexes = list( - string_range_to_list(self.rep_dict[component]["ids"], shift=0)[0] + string_range_to_list(self.rep_dict[component]["ids"], shift=0)[ + 0 + ] ) - + if rep_indexes: mol = self.displayed_structure[rep_indexes] @@ -957,15 +983,15 @@ def update_viewer(self, c=None): self._viewer.add_licorice(opacity=1.0, component=component) elif self.rep_dict[component]["type"] == "hyperball": self._viewer.add_hyperball(opacity=1.0, component=component) - #self._gen_translation_indexes() + # self._gen_translation_indexes() self._viewer.add_unitcell() self._viewer.center() def orient_z_up(self, _=None): try: - #print("inside orient_z_up") + # print("inside orient_z_up") if self.structure is not None: - #print("orienting") + # print("orienting") cell_z = self.structure.cell[2, 2] com = self.structure.get_center_of_mass() def_orientation = self._viewer._camera_orientation @@ -992,7 +1018,7 @@ def orient_z_up(self, _=None): return False else: return True - except AttributeError: + except AttributeError: return True @default("supercell") @@ -1017,8 +1043,8 @@ def _observe_selection(self, _=None): self.configuration_box.selected_index = self.selection_tab_idx def clear_selection(self, _=None): - self.set_trait("selection", list()), - self.set_trait("selection_adv", ""), + self.set_trait("selection", list()), + self.set_trait("selection_adv", ""), def apply_selection(self, _=None): """Apply selection specified in the text field.""" @@ -1114,31 +1140,30 @@ def _valid_structure(self, change): # pylint: disable=no-self-use """Update structure.""" structure = change["value"] if structure is None: - return None # if no structure provided, the rest of the code can be skipped + return None # if no structure provided, the rest of the code can be skipped if isinstance(structure, Atoms): self.pk = None elif isinstance(structure, Node): self.pk = structure.pk - structure = structure.get_ase() + structure = structure.get_ase() else: raise ValueError( "Unsupported type {}, structure must be one of the following types: " "ASE Atoms object, AiiDA CifData or StructureData." ) - self.natoms= len(structure) + self.natoms = len(structure) if "representations" not in structure.arrays: print("Resetting representations") structure.set_array("representations", np.zeros(self.natoms)) return structure - @observe("structure") def _observe_structure(self, change): """Update displayed_structure trait after the structure trait has been modified.""" - if change["new"] is not None: - self.set_trait("displayed_structure", change["new"].repeat(self.supercell)) + if change["new"] is not None: + self.set_trait("displayed_structure", change["new"].repeat(self.supercell)) self.set_trait("cell", change["new"].cell) self.structure = change["new"] else: @@ -1153,8 +1178,6 @@ def _observe_displayed_structure(self, change): # to avoid unnecessary updates # reactivation would require some care - - def d_from(self, operand): point = np.array([float(i) for i in operand[1:-1].split(",")]) return np.linalg.norm(self.structure.positions - point, axis=1) @@ -1331,9 +1354,9 @@ def print_pos(pos): return " ".join([str(i) for i in pos.round(2)]) def add_info(indx, atom): - id_string="Id:" - if indx >=self.natoms: - id_string = "Id-x"+str(int(indx/self.natoms)) + id_string = "Id:" + if indx >= self.natoms: + id_string = "Id-x" + str(int(indx / self.natoms)) return f"{id_string} {indx + 1}; Symbol: {atom.symbol}; Coordinates: ({print_pos(atom.position)})
" # Find geometric center. @@ -1343,13 +1366,19 @@ def add_info(indx, atom): # Report coordinates. if len(self.selection) == 1: - return add_info(self.selection[0], self.displayed_structure[self.selection[0]]) + return add_info( + self.selection[0], self.displayed_structure[self.selection[0]] + ) # Report coordinates, distance and center. if len(self.selection) == 2: info = "" - info += add_info(self.selection[0], self.displayed_structure[self.selection[0]]) - info += add_info(self.selection[1], self.displayed_structure[self.selection[1]]) + info += add_info( + self.selection[0], self.displayed_structure[self.selection[0]] + ) + info += add_info( + self.selection[1], self.displayed_structure[self.selection[1]] + ) dist = self.displayed_structure.get_distance(*self.selection) distv = self.displayed_structure.get_distance(*self.selection, vector=True) info += f"Distance: {dist:.2f} ({print_pos(distv)})
Geometric center: ({geom_center})" @@ -1376,7 +1405,9 @@ def add_info(indx, atom): # Report dihedral angle and geometric center. if len(self.selection) == 4: try: - dihedral = self.displayed_structure.get_dihedral(self.selection) * 180 / np.pi + dihedral = ( + self.displayed_structure.get_dihedral(self.selection) * 180 / np.pi + ) dihedral_str = f"{dihedral:.2f}" except ZeroDivisionError: dihedral_str = "nan"