diff --git a/README.md b/README.md
index f3afeea..8dd1376 100644
--- a/README.md
+++ b/README.md
@@ -81,7 +81,6 @@ This should open your default text editor.
Copy-paste the following profile configuration (substitute path to ORCA and the Docker image name if needed)
```
port = 8888
-default_apps = [ "aiidalab-ispg@git+https://github.com/ispg-group/aiidalab-ispg.git@main",]
system_user = "jovyan"
image = "ghcr.io/ispg-group/atmospec:latest"
home_mount = "aiidalab_atmospec_home"
diff --git a/aiidalab_ispg/app/atmospec_steps.py b/aiidalab_ispg/app/atmospec_steps.py
index beb8a6c..9f5ae80 100644
--- a/aiidalab_ispg/app/atmospec_steps.py
+++ b/aiidalab_ispg/app/atmospec_steps.py
@@ -21,6 +21,7 @@
MoleculeSettings,
ResourceSelectionWidget,
WignerSamplingSettings,
+ OrbitalSettings,
)
from .optimization_steps import OptimizationParameters
from .steps import SubmitWorkChainStepBase, ViewWorkChainStatusStep
@@ -39,6 +40,8 @@ class AtmospecParameters(OptimizationParameters):
tddft_functional: str
nwigner: int
wigner_low_freq_thr: float
+
+ calculate_orbitals: bool
DEFAULT_ATMOSPEC_PARAMETERS = AtmospecParameters(
@@ -54,6 +57,8 @@ class AtmospecParameters(OptimizationParameters):
tddft_functional="wB97X-D4",
nwigner=0,
wigner_low_freq_thr=100.0,
+
+ calculate_orbitals=True,
)
@@ -81,11 +86,15 @@ def __init__(self, **kwargs):
)
self.wigner_settings = WignerSamplingSettings()
+
+ self.orbital_settings = OrbitalSettings()
self.codes_selector = CodeSettings()
self.resources_settings = ResourceSelectionWidget()
self.codes_selector.orca.observe(self._update_state, "value")
+
+ self.codes_selector.orca_plot.observe(self._update_state, "value")
# Set defaults
self._update_ui_from_parameters(DEFAULT_ATMOSPEC_PARAMETERS)
@@ -96,6 +105,8 @@ def __init__(self, **kwargs):
self.wigner_settings,
self.molecule_settings,
self.excited_state_settings,
+
+ self.orbital_settings,
]
grid_layout = ipw.Layout(
width="100%",
@@ -122,6 +133,10 @@ def _validate_input_parameters(self) -> bool:
if self.codes_selector.orca.value is None:
return False
return True
+
+ if self.codes_selector.orca_plot.value is None:
+ return False
+ return True
def _wigner_allowed(self):
# Do not allow Wigner sampling for EOM-CCSD
@@ -188,6 +203,8 @@ def _update_ui_from_parameters(self, parameters: AtmospecParameters) -> None:
self.excited_state_settings.basis.value = parameters.es_basis
self.wigner_settings.nwigner.value = parameters.nwigner
self.wigner_settings.wigner_low_freq_thr.value = parameters.wigner_low_freq_thr
+
+ self.orbital_settings.calculate_orbitals.value = parameters.calculate_orbitals
# Infer the value of the gs_sync checkbox
if (
@@ -213,6 +230,8 @@ def _get_parameters_from_ui(self) -> AtmospecParameters:
nstates=self.excited_state_settings.nstates.value,
nwigner=self.wigner_settings.nwigner.value,
wigner_low_freq_thr=self.wigner_settings.wigner_low_freq_thr.value,
+
+ calculate_orbitals = self.orbital_settings.calculate_orbitals.value,
)
@traitlets.observe("process")
@@ -269,7 +288,7 @@ def build_base_orca_params(self, params: AtmospecParameters) -> dict:
"input_keywords": input_keywords,
}
- def _add_mdci_orca_params(self, orca_parameters, basis, mdci_method, nroots):
+ def _add_mdci_orca_params(self, orca_parameters, basis, mdci_method, nroots, donto):
mdci_params = deepcopy(orca_parameters)
mdci_params["input_keywords"].append(mdci_method.value)
mdci_params["input_keywords"].append(basis)
@@ -280,6 +299,9 @@ def _add_mdci_orca_params(self, orca_parameters, basis, mdci_method, nroots):
mdci_params["input_blocks"]["mdci"] = {
"nroots": nroots,
"maxcore": MEMORY_PER_CPU,
+
+# Calculate nto flag
+ "donto": donto,
}
# TODO: For efficiency reasons, in might not be necessary to calculated left-vectors
# to obtain TDM, but we need to benchmark that first.
@@ -289,7 +311,7 @@ def _add_mdci_orca_params(self, orca_parameters, basis, mdci_method, nroots):
return mdci_params
def _add_tddft_orca_params(
- self, base_orca_parameters, basis, es_method, functional, nroots
+ self, base_orca_parameters, basis, es_method, functional, nroots, donto
):
tddft_params = deepcopy(base_orca_parameters)
tddft_params["input_keywords"].append(functional)
@@ -297,7 +319,12 @@ def _add_tddft_orca_params(
tddft_params["input_blocks"]["tddft"] = {
"nroots": nroots,
"maxcore": MEMORY_PER_CPU,
+
+# Calculate nto flag
+ "donto": donto,
}
+
+
if es_method == ExcitedStateMethod.TDDFT:
tddft_params["input_blocks"]["tddft"]["tda"] = "false"
return tddft_params
@@ -323,7 +350,11 @@ def submit(self, _=None):
builder = AtmospecWorkChain.get_builder()
builder.code = load_code(self.codes_selector.orca.value)
+
+ builder.plot_code = load_code(self.codes_selector.orca_plot.value)
+
builder.structure = self.input_structure
+
base_orca_parameters = self.build_base_orca_params(bp)
gs_opt_parameters = self._add_optimization_orca_params(
base_orca_parameters, basis=bp.basis, gs_method=bp.method
@@ -338,6 +369,8 @@ def submit(self, _=None):
basis=bp.es_basis,
functional=bp.tddft_functional,
nroots=bp.nstates,
+
+ donto=bp.calculate_orbitals,
)
elif bp.excited_method in (
ExcitedStateMethod.ADC2,
@@ -348,6 +381,8 @@ def submit(self, _=None):
basis=bp.es_basis,
mdci_method=bp.excited_method,
nroots=bp.nstates,
+
+ donto=bp.calculate_orbitals,
)
else:
msg = f"Excited method {bp.excited_method} not implemented"
@@ -355,7 +390,7 @@ def submit(self, _=None):
builder.optimize = bp.optimize
builder.opt.orca.parameters = gs_opt_parameters
- builder.exc.orca.parameters = es_parameters
+ builder.exc.orca.parameters = es_parameters
num_proc = self.resources_settings.num_mpi_tasks.value
if num_proc > 1:
@@ -378,6 +413,9 @@ def submit(self, _=None):
# Fetch GBW file from optimization step, to be used as a guess
# for subsequent excited state calculations.
builder.opt.orca.metadata.options.additional_retrieve_list = ["aiida.gbw"]
+
+ # Retrieve .nto and .cube files
+ builder.exc.orca.metadata.options.additional_retrieve_list = ["*.nto", "*.cube"]
# Clean the remote directory by default,
# we're copying back the main output file and gbw file anyway.
diff --git a/aiidalab_ispg/app/input_widgets.py b/aiidalab_ispg/app/input_widgets.py
index 99211f9..3d7204c 100644
--- a/aiidalab_ispg/app/input_widgets.py
+++ b/aiidalab_ispg/app/input_widgets.py
@@ -293,6 +293,33 @@ def reset(self):
self.wigner_low_freq_thr.value = self._LOW_FREQ_THR_DEFAULT
+class OrbitalSettings(ipw.VBox):
+ title = ipw.HTML(
+ """
+
Orbital Calculation
+ """
+ )
+
+ _CALCULATE_ORBITALS_DEFAULT = True
+
+ def __init__(self, **kwargs):
+ style = {"description_width": "initial"}
+ layout = ipw.Layout(max_width="250px")
+
+ self.calculate_orbitals = ipw.Checkbox(
+ value=self._CALCULATE_ORBITALS_DEFAULT,
+ description="Calculate Natural Transition Orbitals",
+ indent=False,
+ layout=layout,
+ )
+
+ super().__init__(
+ children=[self.title, self.calculate_orbitals]
+ )
+
+ def reset(self):
+ self.charge.value = self._CALCULATE_ORBITALS_DEFAULT
+
class CodeSettings(ipw.VBox):
codes_title = ipw.HTML(
"""
@@ -306,17 +333,25 @@ class CodeSettings(ipw.VBox):
# In the order of priority, we will select the default ORCA code from these
# First, we try to use SLURM on local machine, if available
_DEFAULT_ORCA_CODES = ("orca@slurm", "orca@localhost")
+ _DEFAULT_ORCA_PLOT_CODES = ("orca_plot@localhost")
def __init__(self, **kwargs):
self.orca = ComputationalResourcesWidget(
default_calc_job_plugin="orca.orca",
description="ORCA program",
)
+
+ self.orca_plot = ComputationalResourcesWidget(
+ default_calc_job_plugin="orca.orca_plot",
+ description="ORCA plotting program",
+ )
+
super().__init__(
children=[
self.codes_title,
# self.codes_help,
self.orca,
+ self.orca_plot,
],
**kwargs,
)
@@ -341,6 +376,21 @@ def _set_default_codes(self, _=None):
if not self.orca.value:
print("WARNING: ORCA code has not been found locally")
+
+ for code_label in self._DEFAULT_ORCA_PLOT_CODES:
+ try:
+ self.orca_plot.value = load_code(code_label).uuid
+ return
+ except (NotExistent, ValueError):
+ pass
+ except tl.TraitError:
+ # This can happen if one of the code/computers is not configured/enabled or hidden
+ # In practice, this happened to me locally when importing from production DB.
+ # https://github.com/ispg-group/aiidalab-ispg/issues/240
+ pass
+
+ if not self.orca_plot.value:
+ print("WARNING: ORCA_PLOT code has not been found locally")
def reset(self):
self._set_default_codes()
diff --git a/aiidalab_ispg/nto/parsercalcfunction.py b/aiidalab_ispg/nto/parsercalcfunction.py
new file mode 100644
index 0000000..bbdf7c0
--- /dev/null
+++ b/aiidalab_ispg/nto/parsercalcfunction.py
@@ -0,0 +1,112 @@
+# Modified from https://github.com/radi0sus/orca_st
+
+from aiida.engine import calcfunction
+from aiida.orm import Dict
+
+import re
+
+@calcfunction
+def parse_orca_output(nto_folder, filename="aiida.out", threshold=0, states="all"):
+
+ #Convert from Aiida nodes to python datatypes if required.
+ filename = filename.value
+ threshold = threshold.value
+ states = states.value
+
+ # global constants
+ state_string_start = 'TD-DFT/TDA EXCITED STATES' #check for states from here
+ state_string_end = 'TD-DFT/TDA-EXCITATION SPECTRA' #stop reading states from here
+ nto_string_start ='NATURAL TRANSITION ORBITALS' #NTOs start here
+ found_nto = False #found NTOs in orca.out
+
+ #global lists
+ orblist = list() #list of orbital -> orbital transition
+ statedict = dict() #dictionary of states with all transitions
+ selected_statedict = dict() #dictionary of selected states with all transitions and or with those above the threshold
+
+ #for NTO
+ nto_orblist=list() #list of orbital -> orbital transition for NTOs
+ nto_statedict=dict() #dictionary of states with all transitions for NTOs
+
+ #check if threshold is between 0 and 100%, reset if not
+ if threshold:
+ if threshold > 100 or threshold < 0:
+ print("Warning! Threshold out of range. Reset to 0.")
+ threshold=0
+
+ #open a file
+ #check existence
+ try:
+ with nto_folder.open(filename) as input_file:
+ for line in input_file:
+ #start exctract text
+ if state_string_start in line:
+ for line in input_file:
+ #build the state - with several transitions dict: dict[state]=list(orb1 -> orb2, xx%), list(orb3 -> orb4, xx%)
+ #first the state
+ if re.search(r"STATE\s{1,}(\d{1,}):",line):
+ match_state=re.search(r"STATE\s{1,}(\d{1,}):",line)
+ #transitions here in orblist
+ elif re.search(r"\d{1,}[a,b]\s{1,}->\s{1,}\d{1,}[a,b]",line):
+ match_orbs=re.search(r"(\d{1,}[a,b]\s{1,}->\s{1,}\d{1,}[a,b])\s{1,}:\s{1,}(\d{1,}.\d{1,})",line)
+ orblist.append((match_orbs.group(1).replace(" "," "),match_orbs.group(2)))
+ #add orblist to statedict and clear orblist for next state
+ elif re.search(r"^\s*$",line):
+ if orblist:
+ statedict[match_state.group(1)] = orblist
+ orblist=[]
+
+ #exit here
+ elif nto_string_start in line:
+ break
+ elif state_string_end in line:
+ break
+
+ #same for NTOs
+ if re.search(r"FOR STATE\s{1,}(\d{1,})",line):
+ #found NTO in orca.out
+ found_nto = True
+ match_state_nto=re.search(r"FOR STATE\s{1,}(\d{1,})",line)
+ elif re.search(r"\d{1,}[a,b]\s{1,}->\s{1,}\d{1,}[a,b]",line):
+ match_orbs_nto=re.search(r"(\d{1,}[a,b]\s{1,}->\s{1,}\d{1,}[a,b])\s{1,}: n=\s{1,}(\d{1,}.\d{1,})",line)
+ nto_orblist.append((match_orbs_nto.group(1).replace(" ","").split("->"),match_orbs_nto.group(2)))
+
+ #add orblist to statedict and clear orblist for next state
+ elif re.search(r"^\s*$",line):
+ if nto_orblist:
+ nto_statedict[match_state_nto.group(1)] = nto_orblist
+ nto_orblist=[]
+
+ #file not found -> exit here
+ except IOError:
+ print(f"'{filename}'" + " not found")
+ return {}
+
+ #no NTO data in orca.out -> exit here
+ if found_nto == False:
+ print(f"'{nto_string_start}'" + " not found in " + f"'{filename}'")
+ return {}
+
+ #build selected_statedict from statedict with selected states
+ try:
+ if states == 'all':
+ selected_statedict = nto_statedict
+
+ elif re.search(r"\d",states):
+ matchstateslist=(re.findall(r"\d+",states))
+ for elements in matchstateslist:
+ selected_statedict[elements]=nto_statedict[elements]
+
+ except KeyError:
+ print("Warning! State(s) not present. Exit.")
+ return {}
+
+ #remove transitions below threshold from selected_statedict
+ for elements in selected_statedict:
+ transition_list=[]
+ for v in selected_statedict[elements]:
+ if float(v[1])*100 >= threshold:
+ transition_list.append(v)
+ selected_statedict[elements]=transition_list
+
+ return Dict(selected_statedict)
diff --git a/aiidalab_ispg/nto/subworkchains.py b/aiidalab_ispg/nto/subworkchains.py
new file mode 100644
index 0000000..ad9bfb2
--- /dev/null
+++ b/aiidalab_ispg/nto/subworkchains.py
@@ -0,0 +1,96 @@
+from aiida.engine import WorkChain, calcfunction, ToContext, run_get_node
+from aiida.orm import SinglefileData, StructureData, Dict, FolderData, Str, load_code
+from aiida.plugins import CalculationFactory
+from aiida_shell import launch_shell_job
+import io
+import os
+from cubehandler import Cube
+
+#WorkChain to convert to and compress .cube files.
+class NTOProcessingWorkChain(WorkChain):
+ @classmethod
+ def define(cls, spec):
+ super().define(spec)
+ spec.input("nto_folder", valid_type=FolderData, help="Folder containing the ORCA output from OrcaWorkChain.")
+ spec.input("s", valid_type=Str, help="Desired excitation.")
+ spec.input("mo", valid_type=Str, help="Desired orbital number.")
+ spec.output("compressed_cube", valid_type=SinglefileData, help="Compressed cube file")
+ spec.outline(
+ cls.nto_to_cube,
+ cls.cube_compress
+ )
+
+ def nto_to_cube(self):
+ #load orca_plot
+ orca_plot = load_code("orca_plot@localhost")
+ #Define folder with NTOs
+ folder = self.inputs.nto_folder
+ #Define electronic transition.
+ s=(self.inputs.s).value
+ #Define the specific molecular orbital to plot.
+ mo=(self.inputs.mo).value
+ #Create SinglefileData node with orca_plot options (wrapped in a temporary BytesIO file).
+ plot_options_node = SinglefileData(file=io.BytesIO(("1\n1\n3\n0\n5\n7\n2\n"+mo+"\n10\n11\n").encode("utf-8")), filename="plot_input.txt")
+ #Define NTO filename.
+ nto_filename = "aiida.s"+s+".nto"
+ #Create SinglefileData node with NTO data.
+ with folder.open(nto_filename, mode="rb") as nto_file:
+ nto_data_node = SinglefileData(file=nto_file, filename=nto_filename)
+
+
+ #Run orca_plot
+ results, node = launch_shell_job(
+ "orca_plot",
+ arguments=["{nto_data}", "-i"],
+ nodes={"nto_data": nto_data_node, "plot_options": plot_options_node},
+ metadata={"options": {"filename_stdin": plot_options_node.filename}},
+ outputs=["*.cube"]
+ )
+ #Extract the cube file from the results.
+ self.ctx.uncompressed_cube = results["aiida_s"+(s)+"_mo"+(mo)+"a_cube"]
+
+ def cube_compress(self):
+ #Defining the original cube file.
+ orig_file = self.ctx.uncompressed_cube
+
+ #calcfunction required to create the new cube file "In order to preserve data provenance" apparently.
+ compressed_node = calc_compression(orig_file)
+
+
+ #Output the result
+ self.out("compressed_cube", compressed_node)
+
+
+@calcfunction
+def calc_compression(orig_file):
+ #Cubehandler requires a local file to read from, so we create a temporary file (bit of a bodge).
+ temp_in = "temp.cube"
+ #Opening the original cube file.
+ with orig_file.open(mode="rb") as orig_handle:
+ with open(temp_in, "wb") as temp_handle:
+ temp_handle.write(orig_handle.read())
+
+ #Reading the original cube data.
+ orig_cube = Cube.from_file(temp_in)
+
+ #Compress the file
+ orig_cube.reduce_data_density_slicing(points_per_angstrom=2)
+
+ #Create another temporary file to export the compressed file.
+ temp_out = "temp2.cube"
+ orig_cube.write_cube_file(temp_out, low_precision=False)
+
+ #Read the temporary output file back in as a SinglefileData node.
+ with open(temp_out, "rb") as temp2_handle:
+ compressed_node = SinglefileData(temp2_handle, filename="compressed.cube")
+
+ #Clean up temp files.
+ if os.path.exists(temp_in):
+ os.remove(temp_in)
+ if os.path.exists(temp_out):
+ os.remove(temp_out)
+ return(compressed_node)
+
+
+
+
\ No newline at end of file
diff --git a/aiidalab_ispg/workflows/atmospec.py b/aiidalab_ispg/workflows/atmospec.py
index caab295..c05f5d7 100644
--- a/aiidalab_ispg/workflows/atmospec.py
+++ b/aiidalab_ispg/workflows/atmospec.py
@@ -20,6 +20,7 @@
StructureData,
TrajectoryData,
to_aiida_type,
+ FolderData,
)
from aiida.plugins import CalculationFactory, DataFactory, WorkflowFactory
@@ -33,11 +34,22 @@
structures_to_trajectory,
)
+from aiida_shell import launch_shell_job
+
+import tempfile
+
Code = DataFactory("core.code.installed")
OrcaCalculation = CalculationFactory("orca.orca")
OrcaBaseWorkChain = WorkflowFactory("orca.base")
+from aiida.common import CalcInfo, CodeInfo
+
+from ..nto.parsercalcfunction import parse_orca_output
+
+from ..nto.subworkchains import NTOProcessingWorkChain
+
+
class OrcaExcitationWorkChain(OrcaBaseWorkChain):
"""A simple shim for UV/vis excitation in ORCA."""
@@ -47,20 +59,24 @@ def _build_process_label(self) -> str:
@classmethod
def define(cls, spec):
super().define(spec)
+
spec.output(
"excitations",
valid_type=Dict,
required=True,
help="Excitation energies and oscillator strengths from a single-point excitations",
- )
+ )
+
def extract_transitions_from_orca_output(self, orca_output_params):
+
return {
"oscillator_strengths": orca_output_params["etoscs"],
# Orca returns excited state energies in cm^-1
# Perhaps we should do the conversion here,
# to make this less ORCA specific.
"excitation_energies_cm": orca_output_params["etenergies"],
+
}
@process_handler(exit_codes=ExitCode(0), priority=600)
@@ -71,6 +87,7 @@ def add_excitation_output(self, calculation):
)
self.out("excitations", Dict(transitions).store())
+
class OrcaWignerSpectrumWorkChain(WorkChain):
"""Top level workchain for Nuclear Ensemble Approach UV/vis
@@ -92,9 +109,14 @@ def define(cls, spec):
namespace="exc",
exclude=["orca.structure", "orca.code"],
)
+
spec.input("structure", valid_type=(StructureData, TrajectoryData))
+
spec.input("code", valid_type=Code)
-
+
+ spec.input("plot_code", valid_type=Code)
+
+
# Whether to perform geometry optimization
spec.input(
"optimize",
@@ -121,6 +143,8 @@ def define(cls, spec):
required=True,
help="Output parameters from a single-point excitations",
)
+
+
spec.expose_outputs(
RobustOptimizationWorkChain,
namespace="opt",
@@ -142,6 +166,8 @@ def define(cls, spec):
),
cls.excite,
cls.inspect_excitation,
+ cls.nto_calc,
+ cls.nto_collect,
if_(cls.should_run_wigner)(
cls.wigner_sampling,
cls.wigner_excite,
@@ -182,7 +208,61 @@ def excite(self):
calc_exc = self.submit(OrcaExcitationWorkChain, **inputs)
calc_exc.label = "franck-condon-excitation"
- return ToContext(calc_exc=calc_exc)
+ return ToContext(calc_exc=calc_exc)
+
+
+ def nto_calc(self):
+ #Check ORCA output for NTOs.
+ self.ctx.relevant_dict = parse_orca_output(self.ctx.calc_exc.outputs.retrieved, "aiida.out", 5.0)
+ #This dictionary is also used by the visualiser to create the dropdown menus (not implemented yet).
+ #self.out("transition_info", relevant_dict)
+ #If NTOs are found.
+ if self.ctx.relevant_dict != {}:
+ #Dictionary to store the NTOProcessingWorkChain PKs
+ nto_processes = {}
+ #Returns a folder containing all of the compressed cube files.
+ cube_folder = FolderData()
+ #Create a list of tuples containing relevant mo data for each excitation.
+ relevant_items = list(self.ctx.relevant_dict.items())
+ #Iterating through the list.
+ for excitation in relevant_items:
+ #Set excitation.
+ s=excitation[0]
+ for electron_hole_pair in excitation[1]:
+ for moa in electron_hole_pair[0]:
+ builder = NTOProcessingWorkChain.get_builder()
+ builder.nto_folder = self.ctx.calc_exc.outputs.retrieved
+ builder.s = s
+ #Set specific mo.
+ mo = moa[:-1]
+ builder.mo = mo
+ #Submit the workchain.
+ results = self.submit(NTOProcessingWorkChain, builder)
+ #Add the PK to the dictionary.
+ nto_processes["s"+s+"_"+mo] = results
+ #save the dictionary keys for later use.
+ self.ctx.nto_keys = list(nto_processes.keys())
+ #Move to next step when all ntos are processed.
+ return self.to_context(**nto_processes)
+
+
+
+
+
+ def nto_collect(self):
+ #If NTOs found
+ if self.ctx.relevant_dict != {}:
+ #Create folder to contain cubes
+ cube_folder = FolderData()
+ #Iterate through the outputs of the nto processing workchain.
+ for key in self.ctx.nto_keys:
+ node = self.ctx.get(key)
+ if "compressed_cube" in node.outputs:
+ with node.outputs.compressed_cube.open(mode="rb") as file:
+ cube_folder.put_object_from_filelike(file, path=(key))
+ cube_folder.store()
+ #Output where on the database the compressed files are stored.
+ self.report(f"Cube folder PK: {cube_folder.pk}")
def wigner_sampling(self):
self.report(f"Generating {self.inputs.nwigner.value} Wigner geometries")
@@ -257,6 +337,8 @@ def inspect_excitation(self):
self.report("Single point excitation failed :-(")
return self.exit_codes.ERROR_EXCITATION_FAILED
self.out("franck_condon_excitations", calc.outputs.excitations)
+
+
def inspect_wigner_excitation(self):
"""Check whether all wigner excitations succeeded"""
@@ -315,7 +397,10 @@ def launch(self):
)
for conf_id in self.inputs.structure.get_stepids():
inputs.structure = self.inputs.structure.get_step_structure(conf_id)
+
workflow = self.submit(OrcaWignerSpectrumWorkChain, **inputs)
+
+
workflow.label = f"atmospec-conf-{conf_id}"
self.to_context(confs=append_(workflow))
diff --git a/notebooks/atmospec.ipynb b/notebooks/atmospec.ipynb
index 67e2b95..ef6e5b9 100644
--- a/notebooks/atmospec.ipynb
+++ b/notebooks/atmospec.ipynb
@@ -2,11 +2,44 @@
"cells": [
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 1,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/javascript": [
+ "\n",
+ "IPython.OutputArea.prototype._should_scroll = function(lines) {\n",
+ " return false;\n",
+ "}\n",
+ "// Trying to fix Mol viewer inside accordion box,\n",
+ "// padding 15px is the default, reducing it did not help\n",
+ "var styles = `\n",
+ " .p-Collapse-contents { \n",
+ " padding: 15px;\n",
+ " }\n",
+ "`\n",
+ "var styleSheet = document.createElement(\"style\")\n",
+ "styleSheet.innerText = styles\n",
+ "\n",
+ "document.head.appendChild(styleSheet)\n",
+ "\n",
+ "document.title = 'AiiDAlab ATMOSPEC app'\n",
+ "if (document.getElementById('appmode-busy')) {\n",
+ " window.onbeforeunload = function() {return}\n",
+ "}\n"
+ ],
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"%%javascript\n",
+ "\n",
"IPython.OutputArea.prototype._should_scroll = function(lines) {\n",
" return false;\n",
"}\n",
@@ -30,9 +63,27 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 2,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "\n",
+ "\n"
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"%%html\n",
"\n",
@@ -45,9 +96,24 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 3,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "78ced57448754b74ba3a373d5da19b13",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "HTML(value=\"Loaded AiiDA profile 'default'
Hold on to your hats, ATMOSPEC will be here shortly 🚀\")"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"import ipywidgets as ipw\n",
"\n",
@@ -62,9 +128,295 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 4,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/javascript": [
+ "(function(root) {\n",
+ " function now() {\n",
+ " return new Date();\n",
+ " }\n",
+ "\n",
+ " const force = true;\n",
+ "\n",
+ " if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n",
+ " root._bokeh_onload_callbacks = [];\n",
+ " root._bokeh_is_loading = undefined;\n",
+ " }\n",
+ "\n",
+ "const JS_MIME_TYPE = 'application/javascript';\n",
+ " const HTML_MIME_TYPE = 'text/html';\n",
+ " const EXEC_MIME_TYPE = 'application/vnd.bokehjs_exec.v0+json';\n",
+ " const CLASS_NAME = 'output_bokeh rendered_html';\n",
+ "\n",
+ " /**\n",
+ " * Render data to the DOM node\n",
+ " */\n",
+ " function render(props, node) {\n",
+ " const script = document.createElement(\"script\");\n",
+ " node.appendChild(script);\n",
+ " }\n",
+ "\n",
+ " /**\n",
+ " * Handle when an output is cleared or removed\n",
+ " */\n",
+ " function handleClearOutput(event, handle) {\n",
+ " const cell = handle.cell;\n",
+ "\n",
+ " const id = cell.output_area._bokeh_element_id;\n",
+ " const server_id = cell.output_area._bokeh_server_id;\n",
+ " // Clean up Bokeh references\n",
+ " if (id != null && id in Bokeh.index) {\n",
+ " Bokeh.index[id].model.document.clear();\n",
+ " delete Bokeh.index[id];\n",
+ " }\n",
+ "\n",
+ " if (server_id !== undefined) {\n",
+ " // Clean up Bokeh references\n",
+ " const cmd_clean = \"from bokeh.io.state import curstate; print(curstate().uuid_to_server['\" + server_id + \"'].get_sessions()[0].document.roots[0]._id)\";\n",
+ " cell.notebook.kernel.execute(cmd_clean, {\n",
+ " iopub: {\n",
+ " output: function(msg) {\n",
+ " const id = msg.content.text.trim();\n",
+ " if (id in Bokeh.index) {\n",
+ " Bokeh.index[id].model.document.clear();\n",
+ " delete Bokeh.index[id];\n",
+ " }\n",
+ " }\n",
+ " }\n",
+ " });\n",
+ " // Destroy server and session\n",
+ " const cmd_destroy = \"import bokeh.io.notebook as ion; ion.destroy_server('\" + server_id + \"')\";\n",
+ " cell.notebook.kernel.execute(cmd_destroy);\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " /**\n",
+ " * Handle when a new output is added\n",
+ " */\n",
+ " function handleAddOutput(event, handle) {\n",
+ " const output_area = handle.output_area;\n",
+ " const output = handle.output;\n",
+ "\n",
+ " // limit handleAddOutput to display_data with EXEC_MIME_TYPE content only\n",
+ " if ((output.output_type != \"display_data\") || (!Object.prototype.hasOwnProperty.call(output.data, EXEC_MIME_TYPE))) {\n",
+ " return\n",
+ " }\n",
+ "\n",
+ " const toinsert = output_area.element.find(\".\" + CLASS_NAME.split(' ')[0]);\n",
+ "\n",
+ " if (output.metadata[EXEC_MIME_TYPE][\"id\"] !== undefined) {\n",
+ " toinsert[toinsert.length - 1].firstChild.textContent = output.data[JS_MIME_TYPE];\n",
+ " // store reference to embed id on output_area\n",
+ " output_area._bokeh_element_id = output.metadata[EXEC_MIME_TYPE][\"id\"];\n",
+ " }\n",
+ " if (output.metadata[EXEC_MIME_TYPE][\"server_id\"] !== undefined) {\n",
+ " const bk_div = document.createElement(\"div\");\n",
+ " bk_div.innerHTML = output.data[HTML_MIME_TYPE];\n",
+ " const script_attrs = bk_div.children[0].attributes;\n",
+ " for (let i = 0; i < script_attrs.length; i++) {\n",
+ " toinsert[toinsert.length - 1].firstChild.setAttribute(script_attrs[i].name, script_attrs[i].value);\n",
+ " toinsert[toinsert.length - 1].firstChild.textContent = bk_div.children[0].textContent\n",
+ " }\n",
+ " // store reference to server id on output_area\n",
+ " output_area._bokeh_server_id = output.metadata[EXEC_MIME_TYPE][\"server_id\"];\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " function register_renderer(events, OutputArea) {\n",
+ "\n",
+ " function append_mime(data, metadata, element) {\n",
+ " // create a DOM node to render to\n",
+ " const toinsert = this.create_output_subarea(\n",
+ " metadata,\n",
+ " CLASS_NAME,\n",
+ " EXEC_MIME_TYPE\n",
+ " );\n",
+ " this.keyboard_manager.register_events(toinsert);\n",
+ " // Render to node\n",
+ " const props = {data: data, metadata: metadata[EXEC_MIME_TYPE]};\n",
+ " render(props, toinsert[toinsert.length - 1]);\n",
+ " element.append(toinsert);\n",
+ " return toinsert\n",
+ " }\n",
+ "\n",
+ " /* Handle when an output is cleared or removed */\n",
+ " events.on('clear_output.CodeCell', handleClearOutput);\n",
+ " events.on('delete.Cell', handleClearOutput);\n",
+ "\n",
+ " /* Handle when a new output is added */\n",
+ " events.on('output_added.OutputArea', handleAddOutput);\n",
+ "\n",
+ " /**\n",
+ " * Register the mime type and append_mime function with output_area\n",
+ " */\n",
+ " OutputArea.prototype.register_mime_type(EXEC_MIME_TYPE, append_mime, {\n",
+ " /* Is output safe? */\n",
+ " safe: true,\n",
+ " /* Index of renderer in `output_area.display_order` */\n",
+ " index: 0\n",
+ " });\n",
+ " }\n",
+ "\n",
+ " // register the mime type if in Jupyter Notebook environment and previously unregistered\n",
+ " if (root.Jupyter !== undefined) {\n",
+ " const events = require('base/js/events');\n",
+ " const OutputArea = require('notebook/js/outputarea').OutputArea;\n",
+ "\n",
+ " if (OutputArea.prototype.mime_types().indexOf(EXEC_MIME_TYPE) == -1) {\n",
+ " register_renderer(events, OutputArea);\n",
+ " }\n",
+ " }\n",
+ " if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n",
+ " root._bokeh_timeout = Date.now() + 5000;\n",
+ " root._bokeh_failed_load = false;\n",
+ " }\n",
+ "\n",
+ " const NB_LOAD_WARNING = {'data': {'text/html':\n",
+ " \"\\n\"+\n",
+ " \"
\\n\"+\n",
+ " \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n",
+ " \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n",
+ " \"
\\n\"+\n",
+ " \"
\\n\"+\n",
+ " \"- re-rerun `output_notebook()` to attempt to load from CDN again, or
\\n\"+\n",
+ " \"- use INLINE resources instead, as so:
\\n\"+\n",
+ " \"
\\n\"+\n",
+ " \"
\\n\"+\n",
+ " \"from bokeh.resources import INLINE\\n\"+\n",
+ " \"output_notebook(resources=INLINE)\\n\"+\n",
+ " \"\\n\"+\n",
+ " \"
\"}};\n",
+ "\n",
+ " function display_loaded() {\n",
+ " const el = document.getElementById(null);\n",
+ " if (el != null) {\n",
+ " el.textContent = \"BokehJS is loading...\";\n",
+ " }\n",
+ " if (root.Bokeh !== undefined) {\n",
+ " if (el != null) {\n",
+ " el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n",
+ " }\n",
+ " } else if (Date.now() < root._bokeh_timeout) {\n",
+ " setTimeout(display_loaded, 100)\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " function run_callbacks() {\n",
+ " try {\n",
+ " root._bokeh_onload_callbacks.forEach(function(callback) {\n",
+ " if (callback != null)\n",
+ " callback();\n",
+ " });\n",
+ " } finally {\n",
+ " delete root._bokeh_onload_callbacks\n",
+ " }\n",
+ " console.debug(\"Bokeh: all callbacks have finished\");\n",
+ " }\n",
+ "\n",
+ " function load_libs(css_urls, js_urls, callback) {\n",
+ " if (css_urls == null) css_urls = [];\n",
+ " if (js_urls == null) js_urls = [];\n",
+ "\n",
+ " root._bokeh_onload_callbacks.push(callback);\n",
+ " if (root._bokeh_is_loading > 0) {\n",
+ " console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n",
+ " return null;\n",
+ " }\n",
+ " if (js_urls == null || js_urls.length === 0) {\n",
+ " run_callbacks();\n",
+ " return null;\n",
+ " }\n",
+ " console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n",
+ " root._bokeh_is_loading = css_urls.length + js_urls.length;\n",
+ "\n",
+ " function on_load() {\n",
+ " root._bokeh_is_loading--;\n",
+ " if (root._bokeh_is_loading === 0) {\n",
+ " console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n",
+ " run_callbacks()\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " function on_error(url) {\n",
+ " console.error(\"failed to load \" + url);\n",
+ " }\n",
+ "\n",
+ " for (let i = 0; i < css_urls.length; i++) {\n",
+ " const url = css_urls[i];\n",
+ " const element = document.createElement(\"link\");\n",
+ " element.onload = on_load;\n",
+ " element.onerror = on_error.bind(null, url);\n",
+ " element.rel = \"stylesheet\";\n",
+ " element.type = \"text/css\";\n",
+ " element.href = url;\n",
+ " console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n",
+ " document.body.appendChild(element);\n",
+ " }\n",
+ "\n",
+ " for (let i = 0; i < js_urls.length; i++) {\n",
+ " const url = js_urls[i];\n",
+ " const element = document.createElement('script');\n",
+ " element.onload = on_load;\n",
+ " element.onerror = on_error.bind(null, url);\n",
+ " element.async = false;\n",
+ " element.src = url;\n",
+ " console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n",
+ " document.head.appendChild(element);\n",
+ " }\n",
+ " };\n",
+ "\n",
+ " function inject_raw_css(css) {\n",
+ " const element = document.createElement(\"style\");\n",
+ " element.appendChild(document.createTextNode(css));\n",
+ " document.body.appendChild(element);\n",
+ " }\n",
+ "\n",
+ " const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-2.4.3.min.js\"];\n",
+ " const css_urls = [];\n",
+ "\n",
+ " const inline_js = [ function(Bokeh) {\n",
+ " Bokeh.set_log_level(\"info\");\n",
+ " },\n",
+ "function(Bokeh) {\n",
+ " }\n",
+ " ];\n",
+ "\n",
+ " function run_inline_js() {\n",
+ " if (root.Bokeh !== undefined || force === true) {\n",
+ " for (let i = 0; i < inline_js.length; i++) {\n",
+ " inline_js[i].call(root, root.Bokeh);\n",
+ " }\n",
+ "} else if (Date.now() < root._bokeh_timeout) {\n",
+ " setTimeout(run_inline_js, 100);\n",
+ " } else if (!root._bokeh_failed_load) {\n",
+ " console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n",
+ " root._bokeh_failed_load = true;\n",
+ " } else if (force !== true) {\n",
+ " const cell = $(document.getElementById(null)).parents('.cell').data().cell;\n",
+ " cell.output_area.append_execute_result(NB_LOAD_WARNING)\n",
+ " }\n",
+ " }\n",
+ "\n",
+ " if (root._bokeh_is_loading === 0) {\n",
+ " console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n",
+ " run_inline_js();\n",
+ " } else {\n",
+ " load_libs(css_urls, js_urls, function() {\n",
+ " console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n",
+ " run_inline_js();\n",
+ " });\n",
+ " }\n",
+ "}(window));"
+ ],
+ "application/vnd.bokehjs_load.v0+json": "(function(root) {\n function now() {\n return new Date();\n }\n\n const force = true;\n\n if (typeof root._bokeh_onload_callbacks === \"undefined\" || force === true) {\n root._bokeh_onload_callbacks = [];\n root._bokeh_is_loading = undefined;\n }\n\n\n if (typeof (root._bokeh_timeout) === \"undefined\" || force === true) {\n root._bokeh_timeout = Date.now() + 5000;\n root._bokeh_failed_load = false;\n }\n\n const NB_LOAD_WARNING = {'data': {'text/html':\n \"\\n\"+\n \"
\\n\"+\n \"BokehJS does not appear to have successfully loaded. If loading BokehJS from CDN, this \\n\"+\n \"may be due to a slow or bad network connection. Possible fixes:\\n\"+\n \"
\\n\"+\n \"
\\n\"+\n \"- re-rerun `output_notebook()` to attempt to load from CDN again, or
\\n\"+\n \"- use INLINE resources instead, as so:
\\n\"+\n \"
\\n\"+\n \"
\\n\"+\n \"from bokeh.resources import INLINE\\n\"+\n \"output_notebook(resources=INLINE)\\n\"+\n \"\\n\"+\n \"
\"}};\n\n function display_loaded() {\n const el = document.getElementById(null);\n if (el != null) {\n el.textContent = \"BokehJS is loading...\";\n }\n if (root.Bokeh !== undefined) {\n if (el != null) {\n el.textContent = \"BokehJS \" + root.Bokeh.version + \" successfully loaded.\";\n }\n } else if (Date.now() < root._bokeh_timeout) {\n setTimeout(display_loaded, 100)\n }\n }\n\n function run_callbacks() {\n try {\n root._bokeh_onload_callbacks.forEach(function(callback) {\n if (callback != null)\n callback();\n });\n } finally {\n delete root._bokeh_onload_callbacks\n }\n console.debug(\"Bokeh: all callbacks have finished\");\n }\n\n function load_libs(css_urls, js_urls, callback) {\n if (css_urls == null) css_urls = [];\n if (js_urls == null) js_urls = [];\n\n root._bokeh_onload_callbacks.push(callback);\n if (root._bokeh_is_loading > 0) {\n console.debug(\"Bokeh: BokehJS is being loaded, scheduling callback at\", now());\n return null;\n }\n if (js_urls == null || js_urls.length === 0) {\n run_callbacks();\n return null;\n }\n console.debug(\"Bokeh: BokehJS not loaded, scheduling load and callback at\", now());\n root._bokeh_is_loading = css_urls.length + js_urls.length;\n\n function on_load() {\n root._bokeh_is_loading--;\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: all BokehJS libraries/stylesheets loaded\");\n run_callbacks()\n }\n }\n\n function on_error(url) {\n console.error(\"failed to load \" + url);\n }\n\n for (let i = 0; i < css_urls.length; i++) {\n const url = css_urls[i];\n const element = document.createElement(\"link\");\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.rel = \"stylesheet\";\n element.type = \"text/css\";\n element.href = url;\n console.debug(\"Bokeh: injecting link tag for BokehJS stylesheet: \", url);\n document.body.appendChild(element);\n }\n\n for (let i = 0; i < js_urls.length; i++) {\n const url = js_urls[i];\n const element = document.createElement('script');\n element.onload = on_load;\n element.onerror = on_error.bind(null, url);\n element.async = false;\n element.src = url;\n console.debug(\"Bokeh: injecting script tag for BokehJS library: \", url);\n document.head.appendChild(element);\n }\n };\n\n function inject_raw_css(css) {\n const element = document.createElement(\"style\");\n element.appendChild(document.createTextNode(css));\n document.body.appendChild(element);\n }\n\n const js_urls = [\"https://cdn.bokeh.org/bokeh/release/bokeh-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-gl-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-widgets-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-tables-2.4.3.min.js\", \"https://cdn.bokeh.org/bokeh/release/bokeh-mathjax-2.4.3.min.js\"];\n const css_urls = [];\n\n const inline_js = [ function(Bokeh) {\n Bokeh.set_log_level(\"info\");\n },\nfunction(Bokeh) {\n }\n ];\n\n function run_inline_js() {\n if (root.Bokeh !== undefined || force === true) {\n for (let i = 0; i < inline_js.length; i++) {\n inline_js[i].call(root, root.Bokeh);\n }\n} else if (Date.now() < root._bokeh_timeout) {\n setTimeout(run_inline_js, 100);\n } else if (!root._bokeh_failed_load) {\n console.log(\"Bokeh: BokehJS failed to load within specified timeout.\");\n root._bokeh_failed_load = true;\n } else if (force !== true) {\n const cell = $(document.getElementById(null)).parents('.cell').data().cell;\n cell.output_area.append_execute_result(NB_LOAD_WARNING)\n }\n }\n\n if (root._bokeh_is_loading === 0) {\n console.debug(\"Bokeh: BokehJS loaded, going straight to plotting\");\n run_inline_js();\n } else {\n load_libs(css_urls, js_urls, function() {\n console.debug(\"Bokeh: BokehJS plotting callback run at\", now());\n run_inline_js();\n });\n }\n}(window));"
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"# Activate Bokeh\n",
"\n",
@@ -78,9 +430,39 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 5,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/javascript": [
+ "\n",
+ " var style = document.createElement('style');\n",
+ " style.type = 'text/css';\n",
+ " style.innerHTML = ``;\n",
+ " document.head.appendChild(style);\n",
+ " "
+ ],
+ "text/plain": [
+ ""
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "88e1481fda7c476485dc13408ae92b96",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": []
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ }
+ ],
"source": [
"import os\n",
"from importlib.resources import files\n",
@@ -102,9 +484,89 @@
},
{
"cell_type": "code",
- "execution_count": null,
+ "execution_count": 6,
"metadata": {},
- "outputs": [],
+ "outputs": [
+ {
+ "data": {
+ "application/vnd.jupyter.widget-view+json": {
+ "model_id": "5d0e95f01cb140cbb70101aeff22c4d5",
+ "version_major": 2,
+ "version_minor": 0
+ },
+ "text/plain": [
+ "HTML(value='\\n