diff --git a/aiidalab_widgets_base/structures.py b/aiidalab_widgets_base/structures.py index e5f5100b1..6f2fc32d4 100644 --- a/aiidalab_widgets_base/structures.py +++ b/aiidalab_widgets_base/structures.py @@ -376,32 +376,6 @@ def __init__( children=[self.file_upload, supported_formats, self._status_message] ) - def _validate_and_fix_ase_cell(self, ase_structure, vacuum_ang=10.0): - """ - Checks if the ase Atoms object has a cell set, - otherwise sets it to bounding box plus specified "vacuum" space - """ - if not ase_structure: - return None - - cell = ase_structure.cell - - # TODO: Since AiiDA 2.0, zero cell is possible if PBC=false - # so we should honor that here and do not put artificial cell - # around gas phase molecules. - if ( - np.linalg.norm(cell[0]) < 0.1 - or np.linalg.norm(cell[1]) < 0.1 - or np.linalg.norm(cell[2]) < 0.1 - ): - # if any of the cell vectors is too short, consider it faulty - # set cell as bounding box + vacuum_ang - bbox = np.ptp(ase_structure.positions, axis=0) - new_structure = ase_structure.copy() - new_structure.cell = bbox + vacuum_ang - return new_structure - return ase_structure - def _on_file_upload(self, change=None): """When file upload button is pressed.""" for fname, item in change["new"].items(): @@ -444,9 +418,9 @@ def _read_structure(self, fname, content): return TrajectoryData( structurelist=[ StructureData( - ase=self._validate_and_fix_ase_cell(ase_struct) + ase=ase_structure, ) - for ase_struct in structures + for ase_structure in structures ] ) else: @@ -455,7 +429,7 @@ def _read_structure(self, fname, content): """ return None - return self._validate_and_fix_ase_cell(structures[0]) + return structures[0] class StructureExamplesWidget(ipw.VBox): @@ -720,7 +694,6 @@ def _make_ase(self, species, positions, smiles): if len(species) > 2: positions = PCA(n_components=3).fit_transform(positions) atoms = ase.Atoms(species, positions=positions, pbc=False) - atoms.cell = np.ptp(atoms.positions, axis=0) + 10 atoms.center() # We're attaching this info so that it # can be later stored as an extra on AiiDA Structure node. diff --git a/aiidalab_widgets_base/viewers.py b/aiidalab_widgets_base/viewers.py index d1ed774f9..42e805fc3 100644 --- a/aiidalab_widgets_base/viewers.py +++ b/aiidalab_widgets_base/viewers.py @@ -169,47 +169,74 @@ def __init__( self._viewer.observe(self._on_atom_click, names="picked") self._viewer.stage.set_parameters(mouse_preset="pymol") - view_box = ipw.VBox([self._viewer]) + self.view_box = ipw.VBox([self._viewer]) - configuration_tabs_map = { - "Cell": self._cell_tab(), - "Selection": self._selection_tab(), - "Appearance": self._appearance_tab(), - "Download": self._download_tab(), - } + # Constructing configuration box + if configuration_tabs is None: + self._configuration_tabs = [ + "Selection", + "Appearance", + "Lattice", + "Download", + ] + else: + self._configuration_tabs = configuration_tabs - if configure_view is not True: + # For backward compatibility, where the Lattice tab was called Cell. + if "Cell" in self._configuration_tabs: warnings.warn( - "`configure_view` is deprecated, please use `configuration_tabs` instead.", + "`Cell` tab is deprecated, please use `Lattice` instead. Will be removed in the version 3.0.0", DeprecationWarning, stacklevel=2, ) - if not configure_view: - configuration_tabs.clear() + self._configuration_tabs.remove("Cell") + self._configuration_tabs.append("Lattice") - # Constructing configuration box - if configuration_tabs is None: - configuration_tabs = ["Selection", "Appearance", "Cell", "Download"] - if len(configuration_tabs) != 0: - self.configuration_box = ipw.Tab( - layout=ipw.Layout(flex="1 1 auto", width="auto") + if configure_view is not True: + warnings.warn( + "`configure_view` is deprecated, please use `configuration_tabs` instead. Will be removed in the version 3.0.0", + DeprecationWarning, + stacklevel=2, ) - self.configuration_box.children = [ - configuration_tabs_map[tab_title] for tab_title in configuration_tabs - ] + if not configure_view: + self._configuration_tabs.clear() - for i, title in enumerate(configuration_tabs): - self.configuration_box.set_title(i, title) - children = [ipw.HBox([view_box, self.configuration_box])] - view_box.layout = {"width": "60%"} + if len(self._configuration_tabs) != 0: + configuration_box = self._create_tabs(self._configuration_tabs) + children = [ipw.HBox([self.view_box, configuration_box])] + self.view_box.layout = {"width": "60%"} else: - children = [view_box] + children = [self.view_box] if "children" in kwargs: + warnings.warn( + "defining `children` is deprecated, will be removed in the version 3.0.0", + DeprecationWarning, + stacklevel=2, + ) children += kwargs.pop("children") super().__init__(children, **kwargs) + def _create_tabs(self, configuration_tabs): + """Create tabs for configuration box.""" + configuration_tabs_map = { + "Lattice": self._cell_tab(), + "Selection": self._selection_tab(), + "Appearance": self._appearance_tab(), + "Download": self._download_tab(), + } + + configuration_box = ipw.Tab(layout=ipw.Layout(flex="1 1 auto", width="auto")) + configuration_box.children = [ + configuration_tabs_map[tab_title] for tab_title in configuration_tabs + ] + + for i, title in enumerate(self._configuration_tabs): + configuration_box.set_title(i, title) + + return configuration_box + def _selection_tab(self): """Defining the selection tab.""" @@ -317,7 +344,24 @@ def change_camera(change): @tl.observe("cell") def _observe_cell(self, _=None): - # Updtate the Cell and Periodicity. + # Update the widget layout view. + if not self.cell and "Lattice" in self._configuration_tabs: + self._configuration_tabs.remove("Lattice") + + if self.cell and "Lattice" not in self._configuration_tabs: + self._configuration_tabs = ["Lattice"] + self._configuration_tabs + + if len(self._configuration_tabs) != 0: + configuration_box = self._create_tabs(self._configuration_tabs) + self.children = [ipw.HBox([self.view_box, configuration_box])] + self.view_box.layout = {"width": "60%"} + else: + self.children = [self.view_box] + + # Updtate the Lattice tab and Periodicity. + + # For molecules, the cell is Cell([0, 0, 0]) and in the condition is still # `False``. That's why it is able to use `if self.cell` to cover both None and + # not cell defined cases. if self.cell: self.cell_a.value = "a: {:.4f} {:.4f} {:.4f}".format( *self.cell.array[0] @@ -363,22 +407,6 @@ def _observe_cell(self, _=None): self.periodicity.value = ( f"Periodicity: {periodicity_map[tuple(self.structure.pbc)]}" ) - else: - self.cell_a.value = "a:" - self.cell_b.value = "b:" - self.cell_c.value = "c:" - - self.cell_a_length.value = "|a|:" - self.cell_b_length.value = "|b|:" - self.cell_c_length.value = "|c|:" - - self.cell_alpha.value = "α:" - self.cell_beta.value = "β:" - self.cell_gamma.value = "γ:" - - self.cell_spacegroup.value = "" - self.cell_hall.value = "" - self.periodicity.value = "" def _cell_tab(self): self.cell_a = ipw.HTML() @@ -397,8 +425,6 @@ def _cell_tab(self): self.cell_hall = ipw.HTML() self.periodicity = ipw.HTML() - self._observe_cell() - return ipw.VBox( [ ipw.HTML("Length unit: angstrom (Å)"), @@ -789,7 +815,9 @@ def __init__(self, structure=None, **kwargs): @tl.observe("supercell") def repeat(self, _=None): - if self.structure is not None: + # need to check if cell is defined, otherwise it will throw an error + # since it makes no sense to repeat a molecule + if self.structure is not None and self.cell: self.set_trait("displayed_structure", self.structure.repeat(self.supercell)) @tl.validate("structure") @@ -1096,7 +1124,8 @@ def add_info(indx, atom): @tl.observe("displayed_selection") def _observe_displayed_selection_2(self, _=None): - self.selection_info.value = self.create_selection_info() + if self.displayed_selection: + self.selection_info.value = self.create_selection_info() @register_viewer_widget("data.core.folder.FolderData.") diff --git a/tests/conftest.py b/tests/conftest.py index 9844bd207..28632789f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -203,6 +203,14 @@ def structure_data_object(): return structure +@pytest.fixture +def molecule_ase_object(): + """Return an ase molecule object.""" + from ase.build import molecule + + return molecule("H2O") + + @pytest.fixture def bands_data_object(): BandsData = plugins.DataFactory("core.array.bands") # noqa: N806 diff --git a/tests/test_viewers.py b/tests/test_viewers.py index b5c9287db..20ac2263a 100644 --- a/tests/test_viewers.py +++ b/tests/test_viewers.py @@ -104,3 +104,9 @@ def test_structure_data_viwer(structure_data_object): for fmt, out in format_cases: v.file_format.label = fmt assert v._prepare_payload() == out + + +def test_structure_viewer_for_molecule(molecule_ase_object): + """Test the structure viewer widget for a molecule.""" + v = viewers.StructureDataViewer(molecule_ase_object) + assert isinstance(v, viewers.StructureDataViewer)