#!/usr/bin/env python3
# pylint: disable=missing-function-docstring
from __future__ import print_function
import panel as pn
from panel_chemistry . widgets import JSMEEditor
from panel . interact import interact
import os
import sys
from panel_chemistry . pane import NGLViewer
from panel_chemistry . pane . ngl_viewer import EXTENSIONS
import py3Dmol
from panel_chemistry . pane import Py3DMol
import subprocess
import uuid
# from iodata import IOData
# def test_can_construct():
# JSMEEditor()
def cmmde_gui ( ) :
geom = ' '
pn . extension ( " jsme " , sizing_mode = " stretch_width " )
editor = JSMEEditor ( value = " " , height = 500 , format = " smiles " , subscriptions = [ ' smiles ' ] )
# Name your molecule
Molecule_input = pn . widgets . TextInput ( name = " Molecule name " )
id_input = pn . widgets . TextInput ( name = " Input your name " )
charge = pn . widgets . TextInput ( name = " Charge " , value = " 0 " )
mult = pn . widgets . TextInput ( name = " Spin multiplicity " , value = " 1 " )
workdir = os . getenv ( " HOME " ) + " / " + " scr "
# Terminal widget
TextArea = pn . widgets . TextAreaInput ( value = " Computational Molecular and Material Design Environment \n Authors: \n Universitas Pertamina \n Institut Teknologi Sumatera \n Institut Teknologi Bandung \n Masyarakat Komputasi Indonesia \n \n Supported by: \n Konsorsium Pengembangan Sains Komputasi \n " ,
height = 500 , disabled = True
)
# CMMDE software options
software_main = pn . widgets . Select ( name = " Software selections " , value = ' Orca ' , options = [ ' Orca ' , ' XTB ' , ' Dcdftbmd ' , ' Quantum Espresso ' , ' GROMACS ' ] )
software = { ' Orca ' : ' orca ' , ' GROMACS ' : ' gromacs ' , ' Dcdftbmd ' : ' dcdftb ' , ' Quantum Espresso ' : ' qe ' , ' XTB ' : ' xtb ' }
# Orca card
job_orca = pn . widgets . Select ( name = " Job selections " , value = [ ' Single point calculation ' ] , options = [ ' Single point calculation ' , ' Geometry optimization ' , ' Frequency calculation ' , ' TS optimizer ' , ' Nudged elastic band ' ] )
job_orca_dict = { ' Single point calculation ' : ' sp ' , ' Geometry optimization ' : ' opt ' , ' Frequency calculation ' : ' freq ' , ' TS optimizer ' : ' ts ' , ' Nudged elastic band ' : ' neb ' }
method_orca = pn . widgets . Select ( name = " Method selections " , value = ' GFN2-xTB ' , options = [ ' GFN2-xTB ' , ' GFN1-xTB ' , ' B3LYP/def2-svp ' , ' M06/def2-svp ' ] )
method_orca_dict = { ' GFN2-xTB ' : ' XTB2 ' , ' GFN1-xTB ' : ' XTB1 ' , ' DFTB2 ' : ' DFTB2 ' , ' DFTB2-gammah ' : ' DFTB2_gammah ' , ' DFTB3 ' : ' DFTB3 ' , ' DFTB3-diag ' : ' DFTB3-diag ' , ' B3LYP/def2-svp ' : ' B3LYP def2-svp ' , ' M06/def2-svp ' : ' M06 def2-svp ' }
dispersion_cor = pn . widgets . Select ( name = " Dispersion corrections " , value = ' None ' , options = [ ' None ' , ' D3 ' , ' D3BJ ' , ' D4 ' ] )
solvent = pn . widgets . Select ( name = ' Solvent ' , value = ' None ' , options = [ ' None ' , ' water ' , ' acetone ' , ' acetonitrile ' , ' aniline ' , ' benzaldehyde ' , ' benzene ' , ' CH2Cl2 ' , ' CHCl3 ' , ' CS2 ' , ' dioxane ' , ' DMF ' , ' ethanol ' , ' ether ' , ' ethylacetate ' , ' furane ' , ' hexadecane ' , ' hexane ' , ' octanol ' , ' octanol(wet) ' , ' phenol ' , ' toluene ' , ' THF ' ] )
run_orca_btn = pn . widgets . Button ( name = " Run Orca! " , button_type = ' primary ' )
def run_orca ( event ) :
# RunMessage.value = " "
# terminal.clear()
# unik = str(uuid.uuid4().hex)
Folder = workdir + " / " + id_input . value + " / " + Molecule_input . value
os . chdir ( Folder )
job_list = [ job [ i ] for i in job_orca . value ]
jobs = " , " . join ( job_list )
if editor . value == " " :
FileInput . save ( " geom.xyz " )
geom = " geom.xyz "
else :
geom = editor . value
TextArea . value = TextArea . value + " \n " + " Mempersiapkan Struktur 3 Dimensi! "
cmd = subprocess . run ( [ " cmmde.py " , " -i " , " {} " . format ( geom ) , " -s " , " orca " , " -j " , " {} " . format ( jobs ) , " -m " , " {} " . format ( method [ method_orca . value ] ) , " -c " , " {} " . format ( charge . value ) , " -mult " , " {} " . format ( mult . value ) ] , capture_output = True , text = True )
#terminal.subprocess.run("cmmde.py","-i{}".format(editor.value),"-s{}".format(software[software_main.value]), "-j{}".format(jobs), "-m{}".format(method[method_btn.value]))
TextArea . value = TextArea . value + " \n " + " Perhitungan anda telah tersubmit! "
# RunMessage = pn.widgets.StaticText()
run_orca_btn . on_click ( run_orca )
# CMMDE job options
job_main = pn . widgets . Select ( name = " Job selections " , value = ' Single point calculation ' , options = [ ' Single point calculation ' , ' Geometry optimization ' , ' Frequency calculation ' ] )
job = { ' Single point calculation ' : ' sp ' , ' Geometry optimization ' : ' opt ' , ' Frequency calculation ' : ' freq ' }
# CMMDE method options
method_btn = pn . widgets . Select ( name = " Method selections " , value = ' GFN2-xTB ' , options = [ ' GFN2-xTB ' , ' GFN1-xTB ' , ' B3LYP/def2-svp ' , ' M06/def2-svp ' , ' DFTB2 ' , ' DFTB2-gammah ' , ' DFTB3 ' , ' DFTB3-diag ' ] )
method = { ' GFN2-xTB ' : ' XTB2 ' , ' GFN1-xTB ' : ' XTB1 ' , ' DFTB2 ' : ' DFTB2 ' , ' DFTB2-gammah ' : ' DFTB2_gammah ' , ' DFTB3 ' : ' DFTB3 ' , ' DFTB3-diag ' : ' DFTB3-diag ' , ' B3LYP/def2-svp ' : ' B3LYP def2-svp ' , ' M06/def2-svp ' : ' M06 def2-svp ' }
# File input (if you don't want to draw the structure)
FileInput = pn . widgets . FileInput ( title = ' Input structure ' , accept = ' .xyz,.vasp,.pdb ' )
# def fileinput(event):
# if FileInput.value is not None:
# FileInput.save("geom.xyz")
# FileInput.param.watch(fileinput,'value')
# CMMDE running button
Run_btn = pn . widgets . Button ( name = " Run CMMDE! " , button_type = ' primary ' )
# RunMessage = pn.widgets.StaticText()
def run ( event ) :
# RunMessage.value = " "
# terminal.clear()
# unik = str(uuid.uuid4().hex)
# job_list = [job[i] for i in job_main.value]
# jobs = ",".join(job_list)
Folder = workdir + " / " + id_input . value + " / " + Molecule_input . value + " / " + job [ job_main . value ]
if not os . path . exists ( Folder ) :
os . makedirs ( Folder )
if os . path . exists ( " {} /geom.smi " . format ( Folder ) ) :
os . system ( " rm {} /geom.smi " . format ( Folder ) )
os . chdir ( Folder )
if editor . value == " " :
FileInput . save ( " geom.xyz " )
geom = " geom.xyz "
else :
geom = editor . value
TextArea . value = TextArea . value + " \n " + " Mempersiapkan Struktur 3 Dimensi! "
list_commands = [ " cmmde.py " , " -i " , " {} " . format ( geom ) , " -s " , " {} " . format ( software [ software_main . value ] ) , " -j " , " {} " . format ( job [ job_main . value ] ) , " -m " , " {} " . format ( method [ method_btn . value ] ) , " -c " , " {} " . format ( charge . value ) , " -mult " , " {} " . format ( mult . value ) ]
if dispersion_cor . value != ' None ' :
new_commands = [ " -disp " , " {} " . format ( dispersion_cor . value ) ]
for i in new_commands :
list_commands . append ( i )
if solvent . value != ' None ' :
new_commands = [ " -solvent " , " {} " . format ( solvent . value ) ]
for i in new_commands :
list_commands . append ( i )
cmd = subprocess . run ( list_commands , capture_output = True , text = True )
#terminal.subprocess.run("cmmde.py","-i{}".format(editor.value),"-s{}".format(software[software_main.value]), "-j{}".format(jobs), "-m{}".format(method[method_btn.value]))
TextArea . value = TextArea . value + " \n " + " Perhitungan anda telah tersubmit! "
# RunMessage.value = "Perhitungan telah tersubmit!"
download_xyz = pn . widgets . FileDownload ( file = " cmmd.xyz " , filename = " optimized.xyz " )
Run_btn . on_click ( run )
# Check directory button
checkdir_btn = pn . widgets . Button ( name = " Generate work directory " , type = " primary " )
TextWarning = pn . widgets . StaticText ( )
def checkdir ( event ) :
Folder = workdir + " / " + id_input . value + " / " + Molecule_input . value
isExist = os . path . exists ( Folder )
TextWarning . value = " "
if isExist :
TextWarning . value = " Directory exists! Change the molecule name! "
else :
os . makedirs ( Folder )
TextWarning . value = " Successfully create the directory! "
checkdir_btn . on_click ( checkdir )
# Post calculations
post_calc = { ' Frequency calculation ' : ' freq ' , ' Radial distribution function ' : ' rdf ' , ' Mean Square Displacement ' : ' msd ' , ' Time-dependent calculation ' : ' td ' , ' Thermochemistry calculation ' : ' thermo ' , ' Optimized energy ' : ' opt ' , ' IR plot ' : ' ir ' }
post_btn = pn . widgets . Select ( name = " Job Selection " , value = ' Frequency calculation ' , options = [ ' Frequency calculation ' , ' Radial distribution function ' , ' Mean Square Displacement ' , ' Time-dependent calculation ' , ' Thermochemistry calculation ' , ' Optimized energy ' , ' IR plot ' ] )
# Post Calculation CMMDE software options
post_software_main = pn . widgets . Select ( name = " Software selections for post calculations " , value = ' Orca ' , options = [ ' Orca ' , ' Dcdftbmd ' , ' Quantum Espresso ' ] )
post_software = { ' Orca ' : ' orca ' , ' Dcdftbmd ' : ' dcdftb ' , ' Quantum Espresso ' : ' qe ' }
# Post CMMDE method options
post_method_btn = pn . widgets . Select ( name = " Method selections for post calculations " , value = ' GFN2-xTB ' , options = [ ' GFN2-xTB ' , ' GFN1-xTB ' , ' DFTB2 ' , ' DFTB2-gammah ' , ' DFTB3 ' , ' DFTB3-diag ' , ' B3LYP/def2-svp ' ] )
post_method = { ' GFN2-xTB ' : ' XTB2 ' , ' GFN1-xTB ' : ' XTB1 ' , ' DFTB2 ' : ' DFTB2 ' , ' DFTB2-gammah ' : ' DFTB2_gammah ' , ' DFTB3 ' : ' DFTB3 ' , ' DFTB3-diag ' : ' DFTB3-diag ' , ' B3LYP/def2-svp ' : ' B3LYP def2-svp ' }
def post_calculation ( event ) :
Folder = workdir + " / " + id_input . value + " / " + Molecule_input . value
# terminal.clear()
if post_calc [ post_btn . value ] == ' thermo ' and post_software [ post_software_main . value ] == ' orca ' :
# Folder = workdir + "/" + id_input.value + "/" + Molecule_input.value
# os.chdir(Folder)
Post_folder = Folder + " / " + post_calc [ post_btn . value ]
if not os . path . exists ( Post_folder ) :
os . makedirs ( Post_folder )
os . chdir ( Post_folder )
os . system ( " cp {} /freq/cmmd.out . " . format ( Folder ) )
cmd = subprocess . run ( [ " cmmdepost.py " , " -j " , " {} " . format ( post_calc [ post_btn . value ] ) , " -s " , " {} " . format ( post_software [ post_software_main . value ] ) ] , capture_output = True , text = True )
TextArea . value = TextArea . value + " \n " + cmd . stdout
elif post_calc [ post_btn . value ] == ' ir ' and post_software [ post_software_main . value ] == ' orca ' :
# Folder = workdir + "/" + id_input.value + "/" + Molecule_input.value
# os.chdir(Folder)
Post_folder = Folder + " / " + post_calc [ post_btn . value ]
if not os . path . exists ( Post_folder ) :
os . makedirs ( Post_folder )
os . chdir ( Post_folder )
os . system ( " cp {} /freq/cmmd.out . " . format ( Folder ) )
cmd = subprocess . run ( [ " cmmdepost.py " , " -j " , " {} " . format ( post_calc [ post_btn . value ] ) , " -s " , " {} " . format ( post_software [ post_software_main . value ] ) ] )
TextArea . value = TextArea . value + " \n " + " Plot spektrum IR berhasil dilakukan "
# terminal.subprocess.run("cmmdepost.py","-j{}".format(post_calc[post_btn.value]),"-s{}".format(post_software[post_software_main.value]))
elif post_calc [ post_btn . value ] == ' opt ' and post_software [ post_software_main . value ] == ' orca ' :
# Folder = workdir + "/" + id_input.value + "/" + Molecule_input.value
# os.chdir(Folder)
os . chdir ( Folder )
cmd = subprocess . run ( [ " cmmdepost.py " , " -j " , " {} " . format ( post_calc [ post_btn . value ] ) , " -s " , " {} " . format ( post_software [ post_software_main . value ] ) , " -m " , " {} " . format ( post_method [ post_method_btn . value ] ) ] , capture_output = True , text = True )
TextArea . value = TextArea . value + " \n " + cmd . stdout
else :
# Folder = workdir + "/" + id_input.value + "/" + Molecule_input.value
# os.chdir(Folder)
Post_folder = Folder + " / " + post_calc [ post_btn . value ]
if not os . path . exists ( Post_folder ) :
os . makedirs ( Post_folder )
os . chdir ( Post_folder )
list_commands = [ " cmmde.py " , " -i " , " {} /cmmd.xyz " . format ( Folder + " / " + job [ job_main . value ] ) , " -s " , " {} " . format ( post_software [ post_software_main . value ] ) , " -j " , " {} " . format ( post_calc [ post_btn . value ] ) , " -m " , " {} " . format ( post_method [ post_method_btn . value ] ) ]
if dispersion_cor . value != ' None ' :
new_commands = [ " -disp " , " {} " . format ( dispersion_cor . value ) ]
for i in new_commands :
list_commands . append ( i )
if solvent . value != ' None ' :
new_commands = [ " -solvent " , " {} " . format ( solvent . value ) ]
for i in new_commands :
list_commands . append ( i )
cmd = subprocess . run ( list_commands , capture_output = True , text = True )
# terminal.subprocess.run("cmmde.py","-i{}".format("../cmmd.xyz"),"-s{}".format(post_software[post_software_main.value]), "-j{}".format(post_calc[post_btn.value]), "-m{}".format(post_method[post_method_btn.value]))
TextArea . value = TextArea . value + " \n " + cmd . stdout
runpost_btn = pn . widgets . Button ( name = " Run post calculation! " , button_type = ' primary ' )
runpost_btn . on_click ( post_calculation )
# Check the queue progress
def progress ( event ) :
# terminal.clear()
# terminal.subprocess.run("squeue")
cmd = subprocess . run ( [ " squeue " ] , capture_output = True , text = True )
TextArea . value = TextArea . value + " \n " + cmd . stdout
Progress_btn = pn . widgets . Button ( name = " Check queue " , button_type = ' primary ' )
Progress_btn . on_click ( progress )
# Check the calculation progress
def calc_progress ( event ) :
Folder = workdir + " / " + id_input . value + " / " + Molecule_input . value + " / " + job [ job_main . value ]
os . chdir ( Folder )
cmd = subprocess . run ( [ " tail " , " -n " , " 10 " , " cmmd.out " ] , capture_output = True , text = True )
TextArea . value = TextArea . value + " \n " + cmd . stdout
Checkcalc_btn_main = pn . widgets . Button ( name = " Check calculation " , button_type = ' primary ' )
Checkcalc_btn_main . on_click ( calc_progress )
def calc_progress_post ( event ) :
Folder = workdir + " / " + id_input . value + " / " + Molecule_input . value + " / " + post_calc [ post_btn . value ]
os . chdir ( Folder )
cmd = subprocess . run ( [ " tail " , " -n " , " 10 " , " cmmd.out " ] , capture_output = True , text = True )
TextArea . value = TextArea . value + " \n " + cmd . stdout
Checkcalc_btn_post = pn . widgets . Button ( name = " Check post-calculation " , button_type = ' primary ' )
Checkcalc_btn_post . on_click ( calc_progress_post )
# Slab Builder
hkl_input = pn . widgets . TextInput ( name = " Miller index (hkl) " , placeholder = " Example: 100 " )
size_input = pn . widgets . TextInput ( name = " Dimension " , placeholder = " Example: 2x2 " )
layer_input = pn . widgets . TextInput ( name = " Layer " , placeholder = " Example: 2 " )
slabbuilder_btn = pn . widgets . Button ( name = " Build it! " , button_type = " primary " )
Material_input = pn . widgets . TextInput ( name = " Material name " )
Material_upload = pn . widgets . FileInput ( title = ' Input structure ' )
# def materialinput(event):
# if Material_upload.value is not None:
# Material_upload.save("POSCAR")
# Material_upload.param.watch(materialinput,'value')
# Generate material folder button
materialdir_btn = pn . widgets . Button ( name = " Generate work directory " , type = " primary " )
# # Download Button
download_slab = pn . widgets . FileDownload ( file = " slab.vasp " , filename = " slab.vasp " )
download_spec_plot = pn . widgets . FileDownload ( file = " IR.pdf " , filename = " IR.pdf " )
download_spec_raw = pn . widgets . FileDownload ( file = " IR_fit.dat " . format ( workdir + " / " + id_input . value + " / " + Molecule_input . value + " / " + " ir " ) , filename = " IR_fit.dat " )
download_opt_plot = pn . widgets . FileDownload ( file = " optimized.pdf " , filename = " optimized.pdf " )
download_opt_raw = pn . widgets . FileDownload ( file = " optimized.dat " , filename = " optimized.dat " )
def materialgen ( event ) :
Folder = workdir + " / " + id_input . value + " / " + Material_input . value
isExist = os . path . exists ( Folder )
TextWarning . value = " "
if isExist :
TextWarning . value = " Directory exists! Change the material name! "
else :
os . makedirs ( Folder )
TextWarning . value = " Successfully create the directory! "
materialdir_btn . on_click ( materialgen )
def slab_builder ( event ) :
Folder = workdir + " / " + id_input . value + " / " + Material_input . value
os . chdir ( Folder )
Material_upload . save ( " POSCAR " )
# os.system("mv geom.xyz POSCAR")
hkl = hkl_input . value
size = size_input . value
layer = layer_input . value
cmd = subprocess . run ( [ " cmmdepre.py " , " -j " , " surface " , " -hkl " , " {} " . format ( hkl ) , " -s " , " {} " . format ( size ) , " -n " , " {} " . format ( layer ) , " -i " , " POSCAR " ] )
os . system ( " mv slab_ {} .xyz cmmd.xyz " . format ( hkl ) )
os . system ( " mv slab_ {} .vasp slab.vasp " . format ( hkl ) )
cmd = subprocess . run ( [ " echo " , " ( {} )-surface construction done! " . format ( hkl ) ] , capture_output = True , text = True )
TextArea . value = TextArea . value + " \n " + cmd . stdout
# xyzview = py3Dmol.view()
# with open('slab_{}.xyz'.format(hkl),'r') as f:
# xyz = f.read()
# xyzview.addModel(xyz,'xyz')
# xyzview.setStyle('stick')
# xyzviewer.object = xyzview
slabbuilder_btn . on_click ( slab_builder )
# Solution Builder
filename_solute = pn . widgets . TextInput ( title = " Solute name " )
filename_solvent = pn . widgets . TextInput ( title = " Solvent name " )
solute_upload = pn . widgets . FileInput ( accept = " .xyz " , multiple = True )
solvent_upload = pn . widgets . FileInput ( accept = " .xyz " )
def SolutionBuilder ( event ) :
solute = filename_solute . value . split ( " , " )
for i in solute :
solute_folder = workdir + " / " + id_input . value + " / " + " {} " . format ( i )
os . makedirs ( i )
os . chdir ( i )
solute_upload . save ( solute_upload . filename )
solvent = filename_solvent . value
solvent_folder = workdir + " / " + id_input . value + " / " + solvent
os . chdir ( " ../ {} " . format ( solvent_folder ) )
solvent_upload . save ( solvent_upload . filename )
os . chdir ( " ../ " )
cmd = subprocess . run ( [ " cmmde.py " , " -s " , " gromacs " , " -mt " , " {} " . format ( solute_folder ) ] )
solvent = filename_solvent . value
solvent_upload . save ( solvent_upload . filename )
# Visualize the results
xyzview = py3Dmol . view ( )
xyzviewer = Py3DMol ( xyzview , height = 400 , sizing_mode = " stretch_width " , name = " CMMDE viewer " )
def visualize ( event ) :
xyzview = py3Dmol . view ( )
if Material_input . value == " " :
Folder = workdir + " / " + id_input . value + " / " + Molecule_input . value + " / " + job [ job_main . value ]
else :
Folder = workdir + " / " + id_input . value + " / " + Material_input . value
os . chdir ( Folder )
with open ( ' cmmd.xyz ' , ' r ' ) as f :
xyz = f . read ( )
xyzview . addModel ( xyz , ' xyz ' )
xyzview . setStyle ( { ' stick ' : { } , ' sphere ' : { ' scale ' : .30 } } , viewer = ( 0 , 0 ) )
xyzview . zoomTo ( )
xyzviewer . object = xyzview
visual_btn = pn . widgets . Button ( name = " Visualize! " , button_type = ' primary ' )
visual_btn . on_click ( visualize )
def set_background ( color = ' 0xeeeeee ' ) :
xyzview . setBackgroundColor ( color )
xyzviewer . param . trigger ( " object " )
set_background ( " #e6f6ff " )
accent = " #0072B5 "
# background = pn.widgets.ColorPicker(value="#e6f6ff", name="Background")
# pn.bind(set_background, color=background, watch=True)
# def set_style(style="stick"):
# xyzview.setStyle({style: {}})
# xyzview.zoomTo()
# xyzviewer.param.trigger("object")
# set_style("stick")
# style=pn.widgets.RadioButtonGroup(value="stick", options=["stick", "sphere"], name="Style", button_type="success")
# set_style=pn.bind(set_style, style=style, watch=True)
#### Wrap them all together ##########
return pn . template . MaterialTemplate (
sidebar_width = 410 ,
site = " Computational Molecular and Material Design Environment " ,
title = " CMMDE " ,
main = [ TextArea , editor , xyzviewer ] ,
sidebar = [ pn . Card ( id_input , title = " User Information " , collapsed = True ) , pn . Card ( Molecule_input , charge , mult , checkdir_btn , TextWarning , pn . Card ( FileInput , title = " Upload molecule " , collapsed = True ) , title = " Molecule Information " , collapsed = True ) , pn . Card ( Material_input , materialdir_btn , TextWarning , pn . Card ( Material_upload , title = " Unit cell " , collapsed = True ) , hkl_input , size_input , layer_input , pn . Row ( slabbuilder_btn , visual_btn ) , pn . Row ( download_xyz , download_slab ) , title = " Surface Builder " , collapsed = True ) , pn . Card ( software_main , job_main , method_btn , dispersion_cor , solvent , pn . Row ( Run_btn , Progress_btn ) , pn . Row ( Checkcalc_btn_main , visual_btn ) , download_xyz , title = " Main Calculation " , collapsed = True ) , pn . Card ( post_software_main , post_btn , post_method_btn , dispersion_cor , solvent , pn . Row ( runpost_btn , Progress_btn ) , pn . Row ( Checkcalc_btn_post ) , pn . Row ( download_spec_raw , download_spec_plot ) , pn . Row ( download_opt_raw , download_opt_plot ) , title = " Post-Calculation " , collapsed = True ) ] ,
header_background = accent , accent_base_color = accent
)
if __name__ . startswith ( " bokeh " ) :
cmmde_gui ( ) . servable ( )