Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

use displayed_structure to create selection info #371

Merged
merged 34 commits into from
Jan 9, 2023
Merged
Show file tree
Hide file tree
Changes from 26 commits
Commits
Show all changes
34 commits
Select commit Hold shift + click to select a range
6650466
use displayed_structure to create selection info
superstar54 Oct 18, 2022
720ae1f
Merge branch 'master' into fix_supercell_selection
superstar54 Nov 22, 2022
2b369ab
add info_unit_cell_atoms
superstar54 Nov 22, 2022
9bac99b
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 22, 2022
e05a28e
add displayed_selection
superstar54 Nov 22, 2022
fd8c279
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Nov 22, 2022
129fb82
Merge branch 'master' into fix_supercell_selection
superstar54 Dec 12, 2022
94e30b7
Update aiidalab_widgets_base/viewers.py
superstar54 Dec 12, 2022
8b3f5e4
Update aiidalab_widgets_base/viewers.py
superstar54 Dec 12, 2022
a68ac08
do not update selection formula
superstar54 Dec 12, 2022
8f13226
add a bulk pt for test
superstar54 Dec 12, 2022
f90f8b4
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Dec 12, 2022
a157ab8
use try for advance parse_advanced_sel
superstar54 Dec 12, 2022
6f6dbda
Merge branch 'master' into fix_supercell_selection
yakutovicha Dec 14, 2022
2b15fab
Fix dihedral representation
yakutovicha Dec 14, 2022
7311d66
Remove automatic switching to the selection tab.
yakutovicha Dec 14, 2022
cefff7f
Please flake8
yakutovicha Dec 14, 2022
482172d
Update self.selection after self.displayed_selection
yakutovicha Dec 14, 2022
277d80e
Apply selection to display_structure
yakutovicha Dec 14, 2022
e56b5d6
Tests: upload screenshots also in case of failure.
yakutovicha Dec 15, 2022
e746510
Remove leftovers from testing
yakutovicha Dec 15, 2022
ee21af9
fix test
yakutovicha Dec 15, 2022
b50e84c
Trying to test supercell selection
yakutovicha Dec 15, 2022
48d7849
fix
yakutovicha Dec 15, 2022
a13d260
Fix tests
yakutovicha Dec 15, 2022
746b603
Revert changes to the tests
yakutovicha Dec 15, 2022
8505ea6
(tests) More robust cell expansion in z-direction.
yakutovicha Dec 19, 2022
9b6fc31
Slightly increase the size of supercell selection widgets
yakutovicha Jan 5, 2023
f2d5f19
Introduce 'input_selection' trait to interact with editors
yakutovicha Jan 5, 2023
d4fccda
Merge branch 'master' into fix_supercell_selection
yakutovicha Jan 9, 2023
b998203
Fix pre-commit
yakutovicha Jan 9, 2023
58b641a
Update tests/test_notebooks.py
yakutovicha Jan 9, 2023
a2fd1e7
Selected atoms -> Select atoms.
yakutovicha Jan 9, 2023
010c899
Fix tests
yakutovicha Jan 9, 2023
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
188 changes: 103 additions & 85 deletions aiidalab_widgets_base/viewers.py
Original file line number Diff line number Diff line change
Expand Up @@ -170,7 +170,7 @@ class _StructureDataBaseViewer(ipw.VBox):
"""

selection = List(Int)
selection_adv = Unicode()
displayed_selection = List(Int)
supercell = List(Int)
cell = Instance(Cell, allow_none=True)
DEFAULT_SELECTION_OPACITY = 0.2
Expand Down Expand Up @@ -213,12 +213,6 @@ def __init__(
if configuration_tabs is None:
configuration_tabs = ["Selection", "Appearance", "Cell", "Download"]

self.selection_tab_idx = None
if len(configuration_tabs) != 0:
try:
self.selection_tab_idx = configuration_tabs.index("Selection")
except ValueError:
pass
self.configuration_box = ipw.Tab(
layout=ipw.Layout(flex="1 1 auto", width="auto")
)
Expand Down Expand Up @@ -263,16 +257,14 @@ def _selection_tab(self):
# clear_selection.on_click(lambda _: self.set_trait('selection', list())) # lambda cannot contain assignments
clear_selection.on_click(
lambda _: (
self.set_trait("displayed_selection", []),
self.set_trait("selection", []),
self.set_trait("selection_adv", ""),
# self.wrong_syntax.layout.visibility = 'hidden'
)
)
# CLEAR self.wrong_syntax.layout.visibility = 'visible'

# 5. Button to apply selection
apply_selection = ipw.Button(description="Apply selection")
apply_selection.on_click(self.apply_selection)
apply_displayed_selection = ipw.Button(description="Apply selection")
apply_displayed_selection.on_click(self.apply_displayed_selection)

self.selection_info = ipw.HTML()

Expand All @@ -288,7 +280,9 @@ def _selection_tab(self):
<font style="font-style:italic;font-weight:400;">(x>1 and name not [N,O]) or d_from [1,1,1]>2 or id>=10</font>
</p>"""
),
ipw.HBox([copy_to_clipboard, clear_selection, apply_selection]),
ipw.HBox(
[copy_to_clipboard, clear_selection, apply_displayed_selection]
),
self.selection_info,
]
)
Expand Down Expand Up @@ -496,7 +490,7 @@ def _download_tab(self):
def _render_structure(self, change=None):
"""Render the structure with POVRAY."""

if not isinstance(self.structure, Atoms):
if not isinstance(self.displayed_structure, Atoms):
return

self.render_btn.disabled = True
Expand All @@ -505,7 +499,7 @@ def _render_structure(self, change=None):
zfactor = norm(omat[0, 0:3])
omat[0:3, 0:3] = omat[0:3, 0:3] / zfactor

bb = deepcopy(self.structure.repeat(self.supercell))
bb = deepcopy(self.displayed_structure)
bb.pbc = (False, False, False)

for i in bb:
Expand Down Expand Up @@ -633,17 +627,16 @@ def _on_atom_click(self, _=None):
if "atom1" not in self._viewer.picked.keys():
return # did not click on atom
index = self._viewer.picked["atom1"]["index"]
selection = self.selection.copy()
displayed_selection = self.displayed_selection.copy()

if selection:
if index not in selection:
selection.append(index)
if displayed_selection:
if index not in displayed_selection:
displayed_selection.append(index)
else:
selection.remove(index)
displayed_selection.remove(index)
else:
selection = [index]

self.selection = selection
displayed_selection = [index]
self.displayed_selection = displayed_selection

def highlight_atoms(
self,
Expand Down Expand Up @@ -678,30 +671,33 @@ def _default_selection(self):
def _validate_selection(self, provided):
return list(provided["value"])

@observe("selection")
def _observe_selection(self, _=None):
self.highlight_atoms(self.selection)
self._selected_atoms.value = list_to_string_range(self.selection, shift=1)

# if atom is selected from nglview, shift to selection tab
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@yakutovicha why was this code deleted?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't remember the exact motivation. I think it is because the trait doesn't have to be set by nglview. If it is set elsewhere, the switching will still occur, which might be unwanted.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok, that makes sense. Thanks!

if self._selected_atoms.value and self.selection_tab_idx is not None:
self.configuration_box.selected_index = self.selection_tab_idx
@observe("displayed_selection")
def _observe_displayed_selection(self, _=None):
seen = set()
seq = [x % self.natom for x in self.displayed_selection]
self.selection = [x for x in seq if not (x in seen or seen.add(x))]
self.highlight_atoms(self.displayed_selection)

def apply_selection(self, _=None):
def apply_displayed_selection(self, _=None):
"""Apply selection specified in the text field."""
selection_string = self._selected_atoms.value
expanded_selection, syntax_ok = string_range_to_list(
self._selected_atoms.value, shift=-1
)
if not syntax_ok:
# advance slection
try:
sel = self.parse_advanced_sel(condition=self._selected_atoms.value)
sel = list_to_string_range(sel, shift=1)
expanded_selection, syntax_ok = string_range_to_list(sel, shift=-1)
except (IndexError, TypeError, AttributeError):
syntax_ok = False
self.wrong_syntax.layout.visibility = "visible"
# self.wrong_syntax.layout.visibility = 'hidden' if syntax_ok else 'visible'
if syntax_ok:
self.wrong_syntax.layout.visibility = "hidden"
self.selection = expanded_selection
self._selected_atoms.value = (
selection_string # Keep the old string for further editing.
)
self.displayed_selection = expanded_selection
else:
self.selection_adv = selection_string
self.wrong_syntax.layout.visibility = "visible"

def download(self, change=None): # pylint: disable=unused-argument
"""Prepare a structure for downloading."""
Expand Down Expand Up @@ -767,6 +763,7 @@ class StructureDataViewer(_StructureDataBaseViewer):
def __init__(self, structure=None, **kwargs):
super().__init__(**kwargs)
self.structure = structure
self.natom = len(self.structure) if self.structure is not None else 0
# self.supercell.observe(self.repeat, names='value')

@observe("supercell")
Expand Down Expand Up @@ -796,6 +793,7 @@ def _valid_structure(self, change): # pylint: disable=no-self-use
@observe("structure")
def _observe_structure(self, change):
"""Update displayed_structure trait after the structure trait has been modified."""
self.natom = len(self.structure) if self.structure is not None else 0
# Remove the current structure(s) from the viewer.
if change["new"] is not None:
self.set_trait("displayed_structure", change["new"].repeat(self.supercell))
Expand All @@ -812,7 +810,9 @@ def _update_structure_viewer(self, change):
comp_id
) in self._viewer._ngl_component_ids: # pylint: disable=protected-access
self._viewer.remove_component(comp_id)
self.selection = []
self.displayed_selection = (
[]
) # self.selection will be updated automatically
if change["new"] is not None:
self._viewer.add_component(nglview.ASEStructure(change["new"]))
self._viewer.clear()
Expand All @@ -823,15 +823,15 @@ def _update_structure_viewer(self, change):

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)
return np.linalg.norm(self.displayed_structure.positions - point, axis=1)

def name_operator(self, operand):
"""Defining the name operator which will handle atom kind names."""
if operand.startswith("[") and operand.endswith("]"):
names = operand[1:-1].split(",")
elif not operand.endswith("[") and not operand.startswith("]"):
names = [operand]
symbols = self.structure.get_chemical_symbols()
symbols = self.displayed_structure.get_chemical_symbols()
return np.array([i for i, val in enumerate(symbols) if val in names])

def not_operator(self, operand):
Expand All @@ -842,7 +842,9 @@ def not_operator(self, operand):
names = [operand]
return (
"["
+ ",".join(list(set(self.structure.get_chemical_symbols()) - set(names)))
+ ",".join(
list(set(self.displayed_structure.get_chemical_symbols()) - set(names))
)
+ "]"
)

Expand Down Expand Up @@ -894,10 +896,10 @@ def union(opa, opb):
return np.union1d(opa, opb)

operandsdict = {
"x": self.structure.positions[:, 0],
"y": self.structure.positions[:, 1],
"z": self.structure.positions[:, 2],
"id": np.array([atom.index + 1 for atom in self.structure]),
"x": self.displayed_structure.positions[:, 0],
"y": self.displayed_structure.positions[:, 1],
"z": self.displayed_structure.positions[:, 2],
"id": np.array([atom.index + 1 for atom in self.displayed_structure]),
}

operatorsdict = {
Expand Down Expand Up @@ -991,76 +993,92 @@ def union(opa, opb):
def create_selection_info(self):
"""Create information to be displayed with selected atoms"""

if not self.selection:
if not self.displayed_selection:
return ""

def print_pos(pos):
return " ".join([str(i) for i in pos.round(2)])

def add_info(indx, atom):
return f"Id: {indx + 1}; Symbol: {atom.symbol}; Coordinates: ({print_pos(atom.position)})<br>"
return f"<p>Id: {indx + 1}; Symbol: {atom.symbol}; Coordinates: ({print_pos(atom.position)})</p>"

# Unit and displayed cell atoms' selection.
info_selected_atoms = (
f"<p>Selected atoms: {list_to_string_range(self.displayed_selection, shift=1)}</p>"
+ f"<p>Selected unit cell atoms: {list_to_string_range(self.selection, shift=1)}</p>"
)
# Find geometric center.
geom_center = print_pos(
np.average(self.structure[self.selection].get_positions(), axis=0)
np.average(
self.displayed_structure[self.displayed_selection].get_positions(),
axis=0,
)
)

# Report coordinates.
if len(self.selection) == 1:
return add_info(self.selection[0], self.structure[self.selection[0]])
if len(self.displayed_selection) == 1:
return info_selected_atoms + add_info(
self.displayed_selection[0],
self.displayed_structure[self.displayed_selection[0]],
)

# Report coordinates, distance and center.
if len(self.selection) == 2:
if len(self.displayed_selection) == 2:
info = ""
info += add_info(self.selection[0], self.structure[self.selection[0]])
info += add_info(self.selection[1], self.structure[self.selection[1]])
dist = self.structure.get_distance(*self.selection)
distv = self.structure.get_distance(*self.selection, vector=True)
info += f"Distance: {dist:.2f} ({print_pos(distv)})<br>Geometric center: ({geom_center})"
return info

info_natoms_geo_center = (
f"{len(self.selection)} atoms selected<br>Geometric center: ({geom_center})"
)
info += add_info(
self.displayed_selection[0],
self.displayed_structure[self.displayed_selection[0]],
)
info += add_info(
self.displayed_selection[1],
self.displayed_structure[self.displayed_selection[1]],
)
dist = self.displayed_structure.get_distance(*self.displayed_selection)
distv = self.displayed_structure.get_distance(
*self.displayed_selection, vector=True
)
info += f"<p>Distance: {dist:.2f} ({print_pos(distv)})</p><p>Geometric center: ({geom_center})</p>"
return info_selected_atoms + info

info_natoms_geo_center = f"<p>{len(self.displayed_selection)} atoms selected</p><p>Geometric center: ({geom_center})</p>"

# Report angle geometric center and normal.
if len(self.selection) == 3:
angle = self.structure.get_angle(*self.selection).round(2)
if len(self.displayed_selection) == 3:
angle = self.displayed_structure.get_angle(*self.displayed_selection).round(
2
)
normal = np.cross(
*self.structure.get_distances(
self.selection[1],
[self.selection[0], self.selection[2]],
*self.displayed_structure.get_distances(
self.displayed_selection[1],
[self.displayed_selection[0], self.displayed_selection[2]],
mic=False,
vector=True,
)
)
normal = normal / np.linalg.norm(normal)
return f"{info_natoms_geo_center}<br>Angle: {angle}<br>Normal: ({print_pos(normal)})"
return (
info_selected_atoms
+ f"<p>{info_natoms_geo_center}</p><p>Angle: {angle}</p><p>Normal: ({print_pos(normal)})</p>"
)

# Report dihedral angle and geometric center.
if len(self.selection) == 4:
if len(self.displayed_selection) == 4:
try:
dihedral = self.structure.get_dihedral(self.selection) * 180 / np.pi
dihedral = self.displayed_structure.get_dihedral(
*self.displayed_selection
)
dihedral_str = f"{dihedral:.2f}"
except ZeroDivisionError:
dihedral_str = "nan"
return f"{info_natoms_geo_center}<br>Dihedral angle: {dihedral_str}"

return info_natoms_geo_center
return (
info_selected_atoms
+ f"<p>{info_natoms_geo_center}</p><p>Dihedral angle: {dihedral_str}</p>"
)

@observe("selection_adv")
def _observe_selection_adv(self, _=None):
"""Apply the advanced boolean atom selection"""
try:
sel = self.parse_advanced_sel(condition=self.selection_adv)
self._selected_atoms.value = list_to_string_range(sel, shift=1)
self.wrong_syntax.layout.visibility = "hidden"
self.apply_selection()
except (IndexError, TypeError, AttributeError):
self.wrong_syntax.layout.visibility = "visible"
return info_selected_atoms + info_natoms_geo_center

@observe("selection")
def _observe_selection_2(self, _=None):
@observe("displayed_selection")
def _observe_displayed_selection_2(self, _=None):
self.selection_info.value = self.create_selection_info()


Expand Down
9 changes: 6 additions & 3 deletions notebooks/aiida_datatypes_viewers.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -50,9 +50,12 @@
"outputs": [],
"source": [
"# create molecule\n",
"from ase.build import molecule\n",
"from ase.build import molecule, bulk\n",
"m = molecule('H2O')\n",
"m.center(vacuum=2.0)"
"m.center(vacuum=2.0)\n",
"#\n",
"# create bulk Pt\n",
"pt = bulk('Pt', cubic = True)"
]
},
{
Expand All @@ -69,7 +72,7 @@
"outputs": [],
"source": [
"CifData = DataFactory('core.cif')\n",
"s = CifData(ase=m)\n",
"s = CifData(ase=pt)\n",
"vwr = viewer(s.store(), configuration_tabs=['Selection', 'Appearance', 'Cell', 'Download'])\n",
"display(vwr)"
]
Expand Down
Loading