def workflow_stddft(config: dict) -> None: """ Compute the excited states using simplified TDDFT :param workflow_settings: Arguments to compute the oscillators see: `data/schemas/absorption_spectrum.json :returns: None """ # Dictionary containing the general configuration config.update(initialize(config)) # Single Point calculations settings using CP2K mo_paths_hdf5, energy_paths_hdf5 = unpack(calculate_mos(config), 2) # Read structures molecules_au = [ change_mol_units(parse_string_xyz(gs)) for i, gs in enumerate(config.geometries) if (i % config.stride) == 0 ] # Noodles promised call scheduleTDDFT = schedule(compute_excited_states_tddft) results = gather(*[ scheduleTDDFT(config, mo_paths_hdf5[i], DictConfig({ 'i': i * config.stride, 'mol': mol })) for i, mol in enumerate(molecules_au) ]) return run(gather(results, energy_paths_hdf5), folder=config['workdir'])
def workflow_stddft(config: dict) -> None: """ Compute the excited states using simplified TDDFT :param workflow_settings: Arguments to compute the oscillators see: `data/schemas/absorption_spectrum.json :returns: None """ # Dictionary containing the general configuration config.update(initialize(config)) # Single Point calculations settings using CP2K mo_paths_hdf5, energy_paths_hdf5 = unpack(calculate_mos(config), 2) # Read structures molecules_au = [change_mol_units(parse_string_xyz(gs)) for i, gs in enumerate(config.geometries) if (i % config.stride) == 0] # Noodles promised call scheduleTDDFT = schedule(compute_excited_states_tddft) results = gather( *[scheduleTDDFT(config, mo_paths_hdf5[i], DictConfig( {'i': i * config.stride, 'mol': mol})) for i, mol in enumerate(molecules_au)]) return run(gather(results, energy_paths_hdf5), folder=config['workdir'])
def rdf(fn, atoms_i, atoms_j, dr, rmax): """ A function that computes the radial distribution function between a pair of atoms """ # Read the geometries from the MD file geometries = split_file_geometries(fn) molecs = [parse_string_xyz(gs) for gs in geometries] # Get the number of atoms and number of frames in trajectory file n_frames = len(molecs) n_atoms = len(molecs[0]) # Create grid of r values to compute the pair correlation function g_ij r_grid = np.arange(0, rmax, dr) # Used for the histogram n_bins = r_grid.size # Find the indexes in the bond_matrix of the atomic types involved in the calculation of g_ij atoms = np.asarray([molecs[0][i].symbol for i in range(n_atoms)]) index_i = np.where(atoms == atoms_i) index_j = np.where(atoms == atoms_j) rdf_tot = np.zeros(n_bins) for iframe in range(n_frames): # Read coordinates from iframe coords = np.asarray([molecs[iframe][i].xyz for i in range(n_atoms)]) # Compute bond distance matrix for iframe bond_mtx = cdist(coords, coords) # Slice the bond_matrix with only the atom types sliced_mtx = np.triu(bond_mtx[np.ix_(index_i[0], index_j[0])]) # Count the number of atoms within r and r+dr rdf, r_grid = np.histogram(sliced_mtx, bins=n_bins, range=(0, rmax)) # Sum over all rdf rdf_tot += rdf # Center radii r = 0.5 * (r_grid[1:] + r_grid[:-1]) # Compute the volume in a concentric sphere of size dr at a distance r from the origin # elemental volume dV = 4*pi*r^2*dr volume = (4 / 3) * np.pi * (np.power(r_grid[1:], 3) - np.power(r_grid[:-1], 3)) vol_tot = np.sum(volume) # Compute the rho_ab(r) # This is the pair density distribution for each frame rho_ab = rdf_tot / (volume * n_frames) # Compute the bulk density # skip first element otherwise it counts an atom with itself n_tot = np.sum(rdf_tot[1:]) / n_frames rho_bulk = n_tot / vol_tot # Density = counts / volume[1:] # skip the first element dV which is 0 g_ab = rho_ab / rho_bulk # Compute also the potential of mean force w_ab = -np.log(g_ab) # we skip the first element at r=0, which in the histogram is counted many times: # counts an atom with itself return r[1:], g_ab[1:], w_ab[1:]
def test_calc_sphericals(): """Test the calculation of spherical functions.""" with open(PATH_TEST / 'Cd33Se33.xyz', 'r') as f: mol = parse_string_xyz(f.read()) path_hdf5 = PATH_TEST / "Cd33Se33.hdf5" xs = number_spherical_functions_per_atom(mol, "cp2k", "DZVP-MOLOPT-SR-GTH", path_hdf5) expected = np.concatenate((np.repeat(25, 33), np.repeat(13, 33))) assert np.array_equal(xs, expected)
def initialize(config: DictConfig) -> DictConfig: """Initialize all the data required to schedule the workflows. Returns ------- DictConfig Input to run the workflow. """ log_config(config) # Scratch folder scratch_path = create_path_option(config["scratch_path"]) if scratch_path is None: scratch_path = Path(tempfile.gettempdir()) / \ getpass.getuser() / config.project_name logger.warning( f"path to scratch was not defined, using: {scratch_path}") config['workdir'] = scratch_path # If the directory does not exist create it if not scratch_path.exists(): scratch_path.mkdir(parents=True) # Touch HDF5 if it doesn't exists if not os.path.exists(config.path_hdf5): Path(config.path_hdf5).touch() # all_geometries type :: [String] geometries = split_file_geometries(config["path_traj_xyz"]) config['geometries'] = geometries # Create a folder for each point the the dynamics enumerate_from = config["enumerate_from"] len_geometries = len(geometries) config["folders"] = create_point_folder(scratch_path, len_geometries, enumerate_from) config['calc_new_wf_guess_on_points'] = guesses_to_compute( config['calculate_guesses'], enumerate_from, len_geometries) # Generate a list of tuples containing the atomic label # and the coordinates to generate the primitive CGFs atoms = parse_string_xyz(geometries[0]) if 'angstrom' in config["geometry_units"].lower(): atoms = change_mol_units(atoms) # Save Basis to HDF5 save_basis_to_hdf5(config) return config
def initialize(config: dict) -> dict: """ Initialize all the data required to schedule the workflows associated with the nonadaibatic coupling """ log_config(config) # Scratch folder scratch_path = config["scratch_path"] if scratch_path is None: scratch_path = join(tempfile.gettempdir(), getpass.getuser(), config.project_name) logger.warning( f"path to scratch was not defined, using: {scratch_path}") config['workdir'] = scratch_path # If the directory does not exist create it if not os.path.exists(scratch_path): os.makedirs(scratch_path) # HDF5 path path_hdf5 = config["path_hdf5"] if path_hdf5 is None: path_hdf5 = join(scratch_path, 'quantum.hdf5') logger.warning(f"path to the HDF5 was not defined, using: {path_hdf5}") # all_geometries type :: [String] geometries = split_file_geometries(config["path_traj_xyz"]) config['geometries'] = geometries # Create a folder for each point the the dynamics enumerate_from = config["enumerate_from"] len_geometries = len(geometries) config["folders"] = create_point_folder(scratch_path, len_geometries, enumerate_from) config['calc_new_wf_guess_on_points'] = guesses_to_compute( config['calculate_guesses'], enumerate_from, len_geometries) # Generate a list of tuples containing the atomic label # and the coordinates to generate # the primitive CGFs atoms = parse_string_xyz(geometries[0]) if 'angstrom' in config["geometry_units"].lower(): atoms = change_mol_units(atoms) # Save Basis to HDF5 save_basis_to_hdf5(config) return config
def initialize(config: dict) -> dict: """ Initialize all the data required to schedule the workflows associated with the nonadaibatic coupling """ log_config(config) # Scratch folder scratch_path = config["scratch_path"] if scratch_path is None: scratch_path = join(tempfile.gettempdir(), getpass.getuser(), config.project_name) logger.warning("path to scratch was not defined, using: {}".format(scratch_path)) config['workdir'] = scratch_path # If the directory does not exist create it if not os.path.exists(scratch_path): os.makedirs(scratch_path) # HDF5 path path_hdf5 = config["path_hdf5"] if path_hdf5 is None: path_hdf5 = join(scratch_path, 'quantum.hdf5') logger.warning("path to the HDF5 was not defined, using: {}".format(path_hdf5)) # all_geometries type :: [String] geometries = split_file_geometries(config["path_traj_xyz"]) config['geometries'] = geometries # Create a folder for each point the the dynamics enumerate_from = config["enumerate_from"] len_geometries = len(geometries) config["folders"] = create_point_folder( scratch_path, len_geometries, enumerate_from) config['calc_new_wf_guess_on_points'] = guesses_to_compute( config['calculate_guesses'], enumerate_from, len_geometries) # Generate a list of tuples containing the atomic label # and the coordinates to generate # the primitive CGFs atoms = parse_string_xyz(geometries[0]) if 'angstrom' in config["geometry_units"].lower(): atoms = change_mol_units(atoms) # Save Basis to HDF5 save_basis_to_hdf5(config) return config
def workflow_oscillator_strength(workflow_settings: Dict): """ Compute the oscillator strength. :param workflow_settings: Arguments to compute the oscillators see: `data/schemas/absorption_spectrum.json :returns: None """ # Arguments to compute the orbitals and configure the workflow. see: # `data/schemas/general_settings.json config = workflow_settings['general_settings'] # Dictionary containing the general configuration config.update(initialize(**config)) # Point calculations Using CP2K mo_paths_hdf5 = calculate_mos(**config) # geometries in atomic units molecules_au = [change_mol_units(parse_string_xyz(gs)) for gs in config['geometries']] # Construct initial and final states ranges transition_args = [workflow_settings[key] for key in ['initial_states', 'final_states', 'nHOMO']] initial_states, final_states = build_transitions(*transition_args) # Make a promise object the function the compute the Oscillator Strenghts scheduleOscillator = schedule(calc_oscillator_strenghts) oscillators = gather( *[scheduleOscillator( i, mol, mo_paths_hdf5, initial_states, final_states, config) for i, mol in enumerate(molecules_au) if i % workflow_settings['calculate_oscillator_every'] == 0]) energies, promised_cross_section = create_promised_cross_section( oscillators, workflow_settings['broadening'], workflow_settings['energy_range'], workflow_settings['convolution'], workflow_settings['calculate_oscillator_every']) cross_section, data = run( gather(promised_cross_section, oscillators), folder=config['work_dir']) return store_data(data, energies, cross_section)
def run_workflow_stddft(config: DictConfig) -> PromisedObject: """Compute the excited states using simplified TDDFT using `config`.""" # Single Point calculations settings using CP2K mo_paths_hdf5, energy_paths_hdf5 = unpack(calculate_mos(config), 2) # Read structures molecules_au = [ change_mol_units(parse_string_xyz(gs)) for i, gs in enumerate(config.geometries) if (i % config.stride) == 0 ] # Noodles promised call scheduleTDDFT = schedule(compute_excited_states_tddft) results = gather(*[ scheduleTDDFT(config, mo_paths_hdf5[i], DictConfig({ 'i': i * config.stride, 'mol': mol })) for i, mol in enumerate(molecules_au) ]) return gather(results, energy_paths_hdf5)
def select_molecules(config: dict, i: int) -> tuple: """ Select the pairs of molecules to compute the couplings. """ k = 0 if config.overlaps_deph else i return tuple(parse_string_xyz(config.geometries[idx]) for idx in (k, i + 1))
def calculate_overlap(project_name: str, path_hdf5: str, dictCGFs: Dict, geometries: List, mo_paths_hdf5: List, hdf5_trans_mtx: str, enumerate_from: int, overlaps_deph: bool, nHOMO: int = None, couplings_range: Tuple = None, units: str = 'angstrom') -> List: """ Calculate the Overlap matrices before computing the non-adiabatic coupling using 3 consecutive set of MOs in a molecular dynamic. :param path_hdf5: Path to the HDF5 file that contains the numerical results. :type path_hdf5: String :paramter dictCGFS: Dictionary from Atomic Label to basis set :type dictCGFS: Dict String [CGF], CGF = ([Primitives], AngularMomentum), Primitive = (Coefficient, Exponent) :param geometries: list of molecular geometries :param mo_paths: Path to the MO coefficients and energies in the HDF5 file. :param hdf5_trans_mtx: path to the transformation matrix in the HDF5 file. :param enumerate_from: Number from where to start enumerating the folders create for each point in the MD :type enumerate_from: Int :param nHOMO: index of the H**O orbital in the HDF5 :param couplings_range: range of Molecular orbitals used to compute the coupling. :returns: paths to the Overlap matrices inside the HDF5. """ nPoints = len(geometries) - 1 # Inplace scheduling of calculate_overlap function # Equivalent to add @schedule on top of the function schedule_overlaps = schedule(lazy_overlaps) # Compute the Overlaps paths_overlaps = [] for i in range(nPoints): # Extract molecules to compute couplings if overlaps_deph: molecules = tuple( map(lambda idx: parse_string_xyz(geometries[idx]), [0, i + 1])) else: molecules = tuple( map(lambda idx: parse_string_xyz(geometries[idx]), [i, i + 1])) # If units are Angtrom convert then to a.u. if 'angstrom' in units.lower(): molecules = tuple(map(change_mol_units, molecules)) # Compute the coupling overlaps = schedule_overlaps(i, project_name, path_hdf5, dictCGFs, molecules, mo_paths_hdf5, hdf5_trans_mtx=hdf5_trans_mtx, enumerate_from=enumerate_from, nHOMO=nHOMO, couplings_range=couplings_range) paths_overlaps.append(overlaps) # Gather all the promised paths return gather(*paths_overlaps)
def initialize( project_name: str=None, path_traj_xyz: str=None, basis_name: str=None, enumerate_from: int=0, calculate_guesses: int='first', path_hdf5: str=None, scratch_path: str=None, path_basis: str=None, path_potential: str=None, geometry_units: str='angstrom', **kwargs) -> Dict: """ Initialize all the data required to schedule the workflows associated with the nonadaibatic coupling """ # Start logging event file_log = '{}.log'.format(project_name) logging.basicConfig(filename=file_log, level=logging.DEBUG, format='%(levelname)s:%(message)s %(asctime)s\n', datefmt='%m/%d/%Y %I:%M:%S %p') # User variables username = getpass.getuser() # Scratch if scratch_path is None: scratch_path = join('/tmp', username, project_name) logger.warning("path to scratch was not defined, using: {}".format(scratch_path)) # If the directory does not exist create it if not os.path.exists(scratch_path): os.makedirs(scratch_path) # Cp2k configuration files cp2k_config = {"basis": path_basis, "potential": path_potential} # HDF5 path if path_hdf5 is None: path_hdf5 = join(scratch_path, 'quantum.hdf5') logger.warning("path to the HDF5 was not defined, using: {}".format(path_hdf5)) # all_geometries type :: [String] geometries = split_file_geometries(path_traj_xyz) # Create a folder for each point the the dynamics traj_folders = create_point_folder(scratch_path, len(geometries), enumerate_from) if calculate_guesses is None: points_guess = [] elif calculate_guesses.lower() in 'first': # Calculate new Guess in the first geometry points_guess = [enumerate_from] msg = "An initial Calculation will be computed as guess for the wave function" logger.info(msg) else: # Calculate new Guess in each geometry points_guess = [enumerate_from + i for i in range(len(geometries))] msg = "A guess calculation will be done for each geometry" logger.info(msg) # Generate a list of tuples containing the atomic label # and the coordinates to generate # the primitive CGFs atoms = parse_string_xyz(geometries[0]) if 'angstrom' in geometry_units.lower(): atoms = change_mol_units(atoms) # CGFs per element dictCGFs = create_dict_CGFs(path_hdf5, basis_name, atoms, package_config=cp2k_config) # Calculcate the matrix to transform from cartesian to spherical # representation of the overlap matrix hdf5_trans_mtx = store_transf_matrix( path_hdf5, atoms, dictCGFs, basis_name, project_name) d = {'package_config': cp2k_config, 'path_hdf5': path_hdf5, 'calc_new_wf_guess_on_points': points_guess, 'geometries': geometries, 'enumerate_from': enumerate_from, 'dictCGFs': dictCGFs, 'work_dir': scratch_path, 'folders': traj_folders, 'basis_name': basis_name, 'hdf5_trans_mtx': hdf5_trans_mtx} return d
def workflow_electron_transfer(workflow_settings: Dict): """ Use a MD trajectory to calculate the Electron transfer rate. :param workflow_settings: Arguments to compute the oscillators see: `data/schemas/electron_transfer.json :returns: None """ # Arguments to compute the orbitals and configure the workflow. see: # `data/schemas/general_settings.json config = workflow_settings['general_settings'] # Dictionary containing the general configuration config.update(initialize(**config)) # Point calculations Using CP2K mo_paths_hdf5 = calculate_mos(**config) # geometries in atomic units molecules_au = [ change_mol_units(parse_string_xyz(gs)) for gs in config['geometries'] ] # Time-dependent coefficients path_time_coeffs = workflow_settings['path_time_coeffs'] time_depend_coeffs = read_time_dependent_coeffs(path_time_coeffs) msg = "Reading time_dependent coefficients from: {}".format( path_time_coeffs) logger.info(msg) # compute_overlaps_ET scheduled_overlaps = schedule(compute_overlaps_ET) fragment_overlaps = scheduled_overlaps( config['project_name'], molecules_au, config['basis_name'], config['path_hdf5'], config['dictCGFs'], mo_paths_hdf5, workflow_settings['fragment_indices'], config['enumerate_from'], config['package_name']) # Delta time in a.u. dt = workflow_settings['dt'] dt_au = dt * femtosec2au # Indices relation between the PYXAID active space and the orbitals # stored in the HDF5 args_map_index = [ workflow_settings[key] for key in ['orbitals_range', 'pyxaid_HOMO', 'pyxaid_Nmin', 'pyxaid_Nmax'] ] map_index_pyxaid_hdf5 = create_map_index_pyxaid(*args_map_index) # Number of points in the pyxaid trajectory: # shape: (initial_conditions, n_points, n_states) n_points = len(config['geometries']) - 2 # Read the swap between Molecular orbitals obtained from a previous # Coupling calculation swaps = read_swaps(['path_hdf5'], ['project_name']) # Electron transfer rate for each frame of the Molecular dynamics scheduled_photoexcitation = schedule(compute_photoexcitation) etrs = scheduled_photoexcitation(config['path_hdf5'], time_depend_coeffs, fragment_overlaps, map_index_pyxaid_hdf5, swaps, n_points, ['pyxaid_iconds'], dt_au) # Execute the workflow electronTransferRates, path_overlaps = run(gather(etrs, fragment_overlaps), folder=config['work_dir']) for i, mtx in enumerate(electronTransferRates): write_ETR(mtx, dt, i) write_overlap_densities(config['path_hdf5'], path_overlaps, swaps, dt)
def select_molecules(config: DictConfig, i: int) -> Tuple[MolXYZ, MolXYZ]: """Select the pairs of molecules to compute the couplings.""" k = 0 if config.overlaps_deph else i return tuple( parse_string_xyz(config.geometries[idx]) for idx in (k, i + 1))