Skip to content

Commit

Permalink
Merge pull request #169 from aiidalab/release/1.0.0b15
Browse files Browse the repository at this point in the history
Release/1.0.0b15
  • Loading branch information
yakutovicha committed Jan 25, 2021
2 parents 5b2a654 + a70b781 commit 6fe5b06
Show file tree
Hide file tree
Showing 11 changed files with 430 additions and 74 deletions.
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
name: continuous-integration

on:
[push]
[push, pull_request]

jobs:

Expand Down
7 changes: 7 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,13 @@ this folder.

MIT

## Citation

Users of AiiDAlab are kindly asked to cite the following publication in their own work:

A. V. Yakutovich et al., Comp. Mat. Sci. 188, 110165 (2021).
[DOI:10.1016/j.commatsci.2020.110165](https://doi.org/10.1016/j.commatsci.2020.110165)

## Contact

aiidalab@materialscloud.org
Expand Down
2 changes: 1 addition & 1 deletion aiidalab_widgets_base/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,4 @@
from .structures_multi import MultiStructureUploadWidget
from .viewers import viewer

__version__ = "1.0.0b14"
__version__ = "1.0.0b15"
17 changes: 16 additions & 1 deletion aiidalab_widgets_base/computers.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
import shortuuid
import ipywidgets as ipw
from IPython.display import clear_output
from traitlets import Bool, Dict, Instance, Int, Unicode, Union, link, observe, validate
from traitlets import Bool, Dict, Float, Instance, Int, Unicode, Union, link, observe, validate

from aiida.common import NotExistent
from aiida.orm import Computer, QueryBuilder, User
Expand Down Expand Up @@ -485,6 +485,7 @@ class AiidaComputerSetup(ipw.VBox):
append_text = Unicode()
transport = Unicode()
scheduler = Unicode()
safe_interval = Union([Unicode(), Float()])

def __init__(self, **kwargs):
from aiida.transports import Transport
Expand Down Expand Up @@ -532,12 +533,20 @@ def __init__(self, **kwargs):
style=STYLE)
link((inp_computer_ncpus, 'value'), (self, 'mpiprocs_per_machine'))

# Transport type.
inp_transport_type = ipw.Dropdown(value='ssh',
options=Transport.get_valid_transports(),
description="Transport type:",
style=STYLE)
link((inp_transport_type, 'value'), (self, 'transport'))

# Safe interval.
inp_safe_interval = ipw.FloatText(value=30.0,
description='Min. connection interval (sec):',
layout=ipw.Layout(width="270px"),
style=STYLE)
link((inp_safe_interval, 'value'), (self, 'safe_interval'))

# Scheduler.
inp_scheduler = ipw.Dropdown(value='slurm',
options=Scheduler.get_valid_schedulers(),
Expand Down Expand Up @@ -579,6 +588,7 @@ def __init__(self, **kwargs):
inp_mpirun_cmd,
inp_computer_ncpus,
inp_transport_type,
inp_safe_interval,
inp_scheduler,
self._use_login_shell,
]),
Expand All @@ -605,6 +615,7 @@ def _configure_computer(self):
'port': sshcfg.get('port', 22),
'timeout': 60,
'use_login_shell': self._use_login_shell.value,
'safe_interval': self.safe_interval,
}
if 'user' in sshcfg:
authparams['username'] = sshcfg['user']
Expand Down Expand Up @@ -659,6 +670,10 @@ def test(self, _=None):
def _validate_mpiprocs_per_machine(self, provided): # pylint: disable=no-self-use
return int(provided['value'])

@validate('safe_interval')
def _validate_mpiprocs_per_machine(self, provided): # pylint: disable=no-self-use
return float(provided['value'])


class ComputerDropdown(ipw.VBox):
"""Widget to select a configured computer.
Expand Down
118 changes: 117 additions & 1 deletion aiidalab_widgets_base/misc.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
"""Some useful classes used acrross the repository."""

import io
import tokenize
import ipywidgets as ipw
from traitlets import Unicode

Expand Down Expand Up @@ -37,3 +38,118 @@ def copy_to_clipboard(self, change=None): # pylint:disable=unused-argument
""".format(selection=self.value)) # For the moment works for Chrome, but doesn't work for Firefox.
if self.value: # If no value provided - do nothing.
display(javas)


class ReversePolishNotation:
"""Class defining operations for RPN conversion"""

#adapted from:
# Author: Alaa Awad
# Description: program converts infix to postfix notation
#https://gist.github.com/awadalaa/7ef7dc7e41edb501d44d1ba41cbf0dc6
def __init__(self, operators, additional_operands=None):
self.operators = operators
self.additional_operands = additional_operands

def haslessorequalpriority(self, opa, opb):
"""Priority of the different operators"""
if opa not in self.operators:
return False
if opb not in self.operators:
return False
return self.operators[opa]['priority'] <= self.operators[opb]['priority']

def is_operator(self, opx):
"""Identifies operators"""
return opx in self.operators

@staticmethod
def isopenparenthesis(operator):
"""Identifies open paretheses."""
return operator == '('

@staticmethod
def iscloseparenthesis(operator):
"""Identifies closed paretheses."""
return operator == ')'

def convert(self, expr):
"""Convert expression to postfix."""
stack = []
output = []
for char in expr:
if self.is_operator(char) or char in ['(', ')']:
if self.isopenparenthesis(char):
stack.append(char)
elif self.iscloseparenthesis(char):
operator = stack.pop()
while not self.isopenparenthesis(operator):
output.append(operator)
operator = stack.pop()
else:
while stack and self.haslessorequalpriority(char, stack[-1]):
output.append(stack.pop())
stack.append(char)
else:
output.append(char)
while stack:
output.append(stack.pop())
return output

@staticmethod
def parse_infix_notation(condition):
"""Convert a string containing the expression into a list of operators and operands."""
condition = [
token[1] for token in tokenize.generate_tokens(io.StringIO(condition.strip()).readline) if token[1]
]

result = []
open_bracket = False

# Merging lists.
for element in condition:
if element == '[':
res = '['
open_bracket = True
elif element == ']':
res += ']'
result.append(res)
open_bracket = False
elif open_bracket:
res += element
else:
result.append(element)
return result

def execute(self, expression):
"""Execute the provided expression."""

def is_number(string):
"""Check if string is a number. """
try:
float(string)
return True
except ValueError:
return False

stack = []
stackposition = -1
infix_expression = self.parse_infix_notation(expression)
for ope in self.convert(infix_expression):
# Operands.
if is_number(ope):
stack.append(float(ope))
stackposition += 1
elif ope in self.operators:
nargs = self.operators[ope]['nargs']
arguments = [stack[stackposition + indx] for indx in list(range(-nargs + 1, 1))]
stack[stackposition] = self.operators[ope]['function'](*arguments)
del stack[stackposition - nargs + 1:stackposition]
stackposition -= nargs - 1
else:
if self.additional_operands and ope in self.additional_operands:
stack.append(self.additional_operands[ope])
else:
stack.append(ope)
stackposition += 1
return stack[0] if stack else []
121 changes: 72 additions & 49 deletions aiidalab_widgets_base/structures.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import numpy as np
import ipywidgets as ipw
from traitlets import Instance, Int, List, Unicode, Union, dlink, link, default, observe
from sklearn.decomposition import PCA

# ASE imports
import ase
Expand All @@ -18,8 +19,6 @@
from aiida.orm import CalcFunctionNode, CalcJobNode, Data, QueryBuilder, Node, WorkChainNode
from aiida.plugins import DataFactory

from sklearn.decomposition import PCA

# Local imports
from .utils import get_ase_from_file
from .viewers import StructureDataViewer
Expand Down Expand Up @@ -431,7 +430,7 @@ def disable_drop_label(change):
box = ipw.VBox([age_selection, h_line, ipw.HBox([self.mode, self.drop_label])])

self.results = ipw.Dropdown(layout={'width': '900px'})
self.results.observe(self._on_select_structure)
self.results.observe(self._on_select_structure, names='value')
self.search()
super().__init__([box, h_line, self.results])

Expand Down Expand Up @@ -518,80 +517,104 @@ def _on_select_structure(self, _=None):

class SmilesWidget(ipw.VBox):
"""Conver SMILES into 3D structure."""

structure = Instance(Atoms, allow_none=True)

SPINNER = """<i class="fa fa-spinner fa-pulse" style="color:red;" ></i>"""

def __init__(self, title=''):
# pylint: disable=unused-import
self.title = title

try:
import openbabel # pylint: disable=unused-import
from openbabel import pybel
from openbabel import openbabel
except ImportError:
super().__init__(
[ipw.HTML("The SmilesWidget requires the OpenBabel library, "
"but the library was not found.")])
return
try:
from rdkit import Chem
from rdkit.Chem import AllChem
except ImportError:
super().__init__(
[ipw.HTML("The SmilesWidget requires the rdkit library, "
"but the library was not found.")])
return

self.smiles = ipw.Text()
self.smiles = ipw.Text(placeholder='C=C')
self.create_structure_btn = ipw.Button(description="Generate molecule", button_style='info')
self.create_structure_btn.on_click(self._on_button_pressed)
self.output = ipw.HTML("")

super().__init__([self.smiles, self.create_structure_btn, self.output])

@staticmethod
def pymol_2_ase(pymol):
"""Convert pymol object into ASE Atoms."""

species = [chemical_symbols[atm.atomicnum] for atm in pymol.atoms]
pos = np.asarray([atm.coords for atm in pymol.atoms])
pca = PCA(n_components=3)
posnew = pca.fit_transform(pos)
atoms = Atoms(species, positions=posnew, pbc=True, cell=np.ptp(posnew, axis=0) + 10)
def make_ase(self, species, positions):
"""Create ase Atoms object."""
# Get the principal axes and realign the molecule along z-axis.
positions = PCA(n_components=3).fit_transform(positions)
atoms = Atoms(species, positions=positions, pbc=True)
atoms.cell = np.ptp(atoms.positions, axis=0) + 10
atoms.center()
return atoms

def _optimize_mol(self, mol):
"""Optimize a molecule using force field (needed for complex SMILES)."""

# Note, the pybel module imported below comes together with openbabel package. Do not confuse it with
# pybel package available on PyPi: https://pypi.org/project/pybel/
import pybel # pylint:disable=import-error

self.output.value = "Screening possible conformers {}".format(self.SPINNER) #font-size:20em;
return atoms

f_f = pybel._forcefields["uff"] # pylint: disable=protected-access
if not f_f.Setup(mol.OBMol):
f_f = pybel._forcefields["mmff94"] # pylint: disable=protected-access
if not f_f.Setup(mol.OBMol):
self.output.value = "Cannot set up forcefield"
return

# Initial cleanup before the weighted search.
f_f.Setup(mol.OBMol)
f_f.SteepestDescent(5000, 1.0e-9)
f_f.GetCoordinates(mol.OBMol)
self.output.value = ""
def _pybel_opt(self, smile, steps):
"""Optimize a molecule using force field and pybel (needed for complex SMILES)."""
from openbabel import pybel as pb
from openbabel import openbabel as ob
obconversion = ob.OBConversion()
obconversion.SetInFormat('smi')
obmol = ob.OBMol()
obconversion.ReadString(obmol, smile)

pbmol = pb.Molecule(obmol)
pbmol.make3D(forcefield="uff", steps=50)

pbmol.localopt(forcefield="gaff", steps=200)
pbmol.localopt(forcefield="mmff94", steps=100)

f_f = pb._forcefields["uff"] # pylint: disable=protected-access
f_f.Setup(pbmol.OBMol)
f_f.ConjugateGradients(steps, 1.0e-9)
f_f.GetCoordinates(pbmol.OBMol)
species = [chemical_symbols[atm.atomicnum] for atm in pbmol.atoms]
positions = np.asarray([atm.coords for atm in pbmol.atoms])
return self.make_ase(species, positions)

def _rdkit_opt(self, smile, steps):
"""Optimize a molecule using force field and rdkit (needed for complex SMILES)."""
from rdkit import Chem
from rdkit.Chem import AllChem

smile = smile.replace("[", "").replace("]", "")
mol = Chem.MolFromSmiles(smile)
mol = Chem.AddHs(mol)

AllChem.EmbedMolecule(mol, maxAttempts=20, randomSeed=42)
AllChem.UFFOptimizeMolecule(mol, maxIters=steps)
positions = mol.GetConformer().GetPositions()
natoms = mol.GetNumAtoms()
species = [mol.GetAtomWithIdx(j).GetSymbol() for j in range(natoms)]
return self.make_ase(species, positions)

def mol_from_smiles(self, smile, steps=10000):
"""Convert SMILES to ase structure try rdkit then pybel"""
try:
return self._rdkit_opt(smile, steps)
except ValueError:
return self._pybel_opt(smile, steps)

def _on_button_pressed(self, change): # pylint: disable=unused-argument
"""Convert SMILES to ase structure when button is pressed."""
self.output.value = ""

# Note, the pybel module imported below comes together with openbabel package. Do not confuse it with
# pybel package available on PyPi: https://pypi.org/project/pybel/
import pybel # pylint:disable=import-error

if not self.smiles.value:
return

mol = pybel.readstring("smiles", self.smiles.value)
self.output.value = """SMILES to 3D conversion {}""".format(self.SPINNER)
mol.make3D()
mol.addh()

pybel._builder.Build(mol.OBMol) # pylint: disable=protected-access

self._optimize_mol(mol)
self.structure = self.pymol_2_ase(mol)
self.output.value = "Screening possible conformers {}".format(self.SPINNER) #font-size:20em;
self.structure = self.mol_from_smiles(self.smiles.value)
self.output.value = ""

@default('structure')
def _default_structure(self):
Expand Down

0 comments on commit 6fe5b06

Please sign in to comment.