def set_cp2k_param(settings: Settings, param_dict: dict) -> None: """Placeholder.""" for block_name, block in param_dict.items(): # Create a to-be formatted string with user-specified units unit = f'[{block.unit}]' + ' {}' if 'unit' in block else '{}' # Get the to-be update list of settings s = settings.get_nested(block['keys']) if not isinstance(s, list): _s = settings.get_nested(block['keys'][:-1]) s = _s[block['keys'][-1]] = [] for k, v in block.items(): if k in ('keys', 'unit'): # Skip continue value = unit.format(v) atom = 'atoms' if len(k.split()) > 1 else 'atom' atom_list = [i[atom] for i in s] try: # Intersecting set idx = atom_list.index(k) s[idx].update({block_name: value}) except ValueError: # Disjoint set new_block = Settings({atom: k, block_name: value}) s.append(new_block)
def recreate_settings(self) -> Settings: """Construct a |Settings| instance from ``"$JN.run"``.""" runfile = self['$JN.run'] # Ignore the first 2 lines with open(runfile, 'r') as f: for i in f: if 'MATCH.pl' in i: args = next(f).split() break else: raise FileError( f"Failed to parse the content of ...{os.sep}{runfile!r}") # Delete the executable and pop the .pdb filename del args[0] pdb_file = args.pop(-1) s = Settings() for k, v in zip(args[0::2], args[1::2]): k = k[1:].lower() s.input[k] = v s.input.filename = pdb_file return s
def get_surface_charge_adf(mol: Molecule, job: Type[Job], s: Settings) -> Settings: """Perform a gas-phase ADF single point and return settings for a COSMO-ADF single point. The previous gas-phase calculation as moleculair fragment. Parameters ---------- mol : |plams.Molecule|_ A PLAMS Molecule. job : |Callable|_ A type Callable of a class derived from :class:`Job`, e.g. :class:`AMSJob` or :class:`Cp2kJob`. s : |plams.Settings|_ The settings for **job**. Returns ------- |plams.Settings|_ A new Settings intance, constructed from **s**, suitable for DFT COSMO-RS calculations. """ s.input.allpoints = '' results = mol.job_single_point(job, s, ret_results=True) coskf = get_coskf(results) for at in mol: at.properties.adf.fragment = 'gas' s.update(get_template('qd.yaml')['COSMO-ADF']) s.input.fragments.gas = coskf return s
def update_adf_defaults(ams_settings: Settings) -> None: """Add the COSMO-RS compound defaults to *ams_settings*, returning a copy of the new settings. The engine block (*ams_settings.input.adf*) is soft updated with the following Settings: .. code:: Engine ADF Basis Type TZP Core Small End XC GGA BP86 End Relativity Level Scalar End BeckeGrid Quality Good End EndEngine """ # Find the solvation key # solvation = ams_settings.input.adf.find_case('solvation') # solvation_block = ams_settings.input.adf[solvation] adf = ams_settings.input.find_case('adf') adf_block = ams_settings.input[adf] # Find all keys for within the adf block keys = ('basis', 'xc', 'relativity', 'BeckeGrid') basis, xc, relativity, BeckeGrid = [ adf_block.find_case(item) for item in keys ] # Construct the default solvation block adf_defaults_block = Settings({ basis: { 'Type': 'TZP', 'Core': 'Small' }, xc: { 'GGA': 'BP86' }, relativity: { 'Level': 'Scalar' }, BeckeGrid: { 'Quality': 'Good' } }) # Copy ams_settings and perform a soft update ret = ams_settings.copy() ret.input.adf.soft_update(adf_defaults_block) return ret
def get_template(template_name: str, from_cat_data: bool = True) -> Settings: """Grab a yaml template and return it as Settings object.""" if from_cat_data: path = join('data/templates', template_name) xs = pkg.resource_string('CAT', path) return Settings(yaml.load(xs.decode(), Loader=yaml.FullLoader)) with open(template_name, 'r') as file: return Settings(yaml.load(file, Loader=yaml.FullLoader))
def get_pka(mol: Molecule, coskf_mol: Optional[str], coskf_mol_conj: Optional[str], water: str, hydronium: str, job: Type[Job], s: Settings) -> List[float]: if coskf_mol is None: return 5 * [np.nan] elif coskf_mol_conj is None: return 5 * [np.nan] s = Settings(s) s.input.compound[1]._h = water s.ignore_molecule = True s_dict = {} for name, coskf in _iter_coskf(coskf_mol_conj, coskf_mol, water, hydronium): _s = s.copy() _s.name = name _s.input.compound[0]._h = coskf s_dict[name] = _s # Run the job mol_name = mol.properties.name job_list = [CRSJob(settings=s, name=name) for name, s in s_dict.items()] results_list = [_crs_run(job, mol_name) for job in job_list] # Extract solvation energies and activity coefficients E_solv = {} for name, results in zip(("acid", "base", "solvent", "solvent_conj"), results_list): results.wait() try: E_solv[name] = _E = results.get_energy() assert _E is not None logger.info(f'{results.job.__class__.__name__}: {mol_name} pKa ' f'calculation ({results.job.name}) is successful') except Exception: logger.error(f'{results.job.__class__.__name__}: {mol_name} pKa ' f'calculation ({results.job.name}) has failed') E_solv[name] = np.nan try: mol.properties.job_path += [ join(job.path, job.name + '.in') for job in job_list ] except IndexError: # The 'job_path' key is not available mol.properties.job_path = [ join(job.path, job.name + '.in') for job in job_list ] ret = [E_solv[k] for k in ("acid", "base", "solvent", "solvent_conj")] ret.append(_get_pka(**E_solv)) return ret
def copy(self, deep: bool = False) -> 'FrozenSettings': """Create a copy of this instance. The returned instance is a recursive copy, the **deep** parameter only affecting the function used for copying non-:class:`dict` values: :func:`copy.copy` if ``deep=False`` and :func:`copy.deepcopy` if ``deep=True``. """ copy_func = copy.deepcopy if deep else copy.copy ret = type(self)() for key, value in self.items(): value_cp = copy_func(value) Settings.__setitem__(ret, key, value_cp) return ret
def qd_opt(mol: Molecule, jobs: Tuple[Optional[Type[Job]], ...], settings: Tuple[Optional[Settings], ...], use_ff: bool = False) -> None: """Perform an optimization of the quantum dot. Performs an inplace update of **mol**. Parameters ---------- mol : |plams.Molecule|_ The to-be optimized molecule. job_recipe : |plams.Settings|_ A Settings instance containing all jon settings. Expects 4 keys: ``"job1"``, ``"job2"``, ``"s1"``, ``"s2"``. forcefield : bool If ``True``, perform the job with CP2K with a user-specified forcefield. """ # Prepare the job settings if use_ff: qd_opt_ff(mol, jobs, settings) return None # Expand arguments job1, job2 = jobs s1, s2 = settings # Extra options for AMSJob if job1 is AMSJob: s1 = Settings(s1) s1.input.ams.constraints.atom = mol.properties.indices if job2 is AMSJob: s2 = Settings(s2) s2.input.ams.constraints.atom = mol.properties.indices # Run the first job and fix broken angles mol.job_geometry_opt(job1, s1, name='QD_opt_part1') fix_carboxyl(mol) fix_h(mol) mol.round_coords() # Run the second job if job2 is not None: mol.job_geometry_opt(job2, s2, name='QD_opt_part2') mol.round_coords() return None
def read_mol_folder(mol_dict: Settings) -> Optional[List[Molecule]]: """Read all files (.xyz, .pdb, .mol, .txt or further subfolders) within a folder.""" try: mol_type = 'input_cores' if mol_dict.is_core else 'input_ligands' _file_list = os.listdir(mol_dict.mol) optional_dict = Settings( {k: v for k, v in mol_dict.items() if k not in ('mol', 'path')}) file_list = [{i: optional_dict} for i in _file_list] validate_mol(file_list, mol_type, mol_dict.path) return read_mol(file_list) except Exception as ex: print_exception('read_mol_folder', mol_dict.name, ex)
def repr_Settings(self, obj: Settings, level: int) -> str: # noqa: N802 """Create a :class:`str` representation of a |plams.Settings| instance.""" n = len(obj) if not obj: return f'{obj.__class__.__name__}()' elif level <= 0: return '\n...' pieces: List[str] = [] indent = 4 * ' ' newlevel = level - 1 for k, v in islice(obj.items(), self.maxSettings): key = str(k) value = self.repr1(v, newlevel) pieces.append(f'\n{key}:') if type(obj) is type(value): pieces.append(f'{textwrap.indent(value, indent)}:') else: pieces.append(f'{textwrap.indent(value, indent)}') if n > self.maxSettings: pieces.append('\n...') ret = ''.join(pieces) if level == self.maxlevel: return f'{obj.__class__.__name__}(\n{textwrap.indent(ret[1:], indent)}\n)' return ret
def set_header(s: Settings, *values: str) -> None: """Assign *value* to the ``["_h"]`` key in *s.input.compound*.""" s.input.compound = [] for item in values: s.input.compound.append(Settings({'_h': item})) s.input.compound[ 0].frac1 = 1.0 # The first item in *values should be the solvent
def _overlay_s_plams(self, lj: Iterable[Mapping], sigma_dict: MutableMapping, epsilon_dict: MutableMapping) -> None: """Extract PLAMS-style settings from **lj** and put them in **sigma_dict** and **epsilon_dict**.""" # noqa: E501 for block in lj: with Settings.suppress_missing(): atoms = tuple(block['atoms'].split()) try: unit_sigma, sigma = block['sigma'].split() except ValueError: unit_sigma, sigma = '[angstrom]', block['sigma'] except (TypeError, KeyError): unit_sigma = sigma = None try: unit_eps, epsilon = block['epsilon'].split() except ValueError: unit_eps, epsilon = '[kcalmol]', block['sigma'] except (TypeError, KeyError): unit_eps = epsilon = None if sigma is not None: unit_sigma = unit_sigma[1:-1] unit_sigma = self.UNIT_MAPPING.get(unit_sigma, unit_sigma) sigma_dict[unit_sigma][atoms] = float(sigma) if epsilon is not None: unit_eps = unit_eps[1:-1] unit_eps = self.UNIT_MAPPING.get(unit_eps, unit_eps) epsilon_dict[unit_eps][atoms] = float(epsilon)
def frequencies(): s = Settings() s.input.ams.properties.NormalModes = 'Yes' s.input.ams.Properties.PESPointCharacter = 'No' s.input.ams.NormalModes.ReScanFreqRange = '-1000 0' s.input.ams.PESPointCharacter.NegativeEigenvalueTolerance = -0.001 return s
def DFTB(): s = Settings() s.input.ams.task = 'GeometryOptimization' s.input.DFTB s.input.DFTB.Model = "GFN1-xTB" s.input.ams.System.Charge = 0 return s
def get_surface_charge(mol: Molecule, job: Type[Job], s: Settings) -> Optional[str]: """Construct the COSMO surface of the **mol**. Parameters ---------- mol : |plams.Molecule|_ A PLAMS Molecule. job : |Callable|_ A type Callable of a class derived from :class:`Job`, e.g. :class:`AMSJob` or :class:`Cp2kJob`. s : |plams.Settings|_ The settings for **job**. Returns ------- |plams.Settings|_ Optional: The path+filename of a file containing COSMO surface charges. """ s = Settings(s) # Special procedure for ADF jobs # Use the gas-phase electronic structure as a fragment for the COSMO single point if job is ADFJob: s = get_surface_charge_adf(mol, job, s) s.runscript.post = '$ADFBIN/cosmo2kf "mopac.cos" "mopac.coskf"' results = mol.job_single_point(job, s, ret_results=True) return get_coskf(results)
def _md2opt(s: Settings) -> Settings: """Convert CP2K MD settings to CP2K geometry optimization settings.""" s2 = s.copy() del s2.input.motion.md s2.input['global'].run_type = 'geometry_optimization' # Delete all user-specified parameters; rely on MATCH del s2.input.force_eval.mm.forcefield.charge del s2.input.force_eval.mm.forcefield.nonbonded return s2
def read_mol_smiles(mol_dict: Settings) -> Optional[Molecule]: """Read a SMILES string.""" try: mol = _from_smiles(mol_dict.mol) mol.properties.smiles = Chem.CanonSmiles(mol_dict.mol) if mol_dict.get('indices'): for i in mol_dict.indices: mol[i].properties.anchor = True canonicalize_mol(mol) if mol_dict.get('indices'): mol_dict.indices = tuple(i for i, at in enumerate(mol, 1) if at.properties.pop('anchor', False)) if mol_dict.guess_bonds and not mol_dict.is_qd: mol.guess_bonds() return mol except Exception as ex: print_exception('read_mol_smiles', mol_dict.name, ex)
def get_recipe(self) -> Dict[Tuple[str], JobRecipe]: """Create a recipe for :meth:`WorkFlow.to_db`.""" settings_names = [i[1:] for i in self.export_columns if i[0] == 'settings'] uff_fallback = { 'key': f'RDKit_{rdkit.__version__}', 'value': f'{UFF.__module__}.{UFF.__name__}' } ret: Dict[Tuple[str], JobRecipe] = Settings() for name, job, settings in zip(settings_names, self.jobs, self.settings): # job is None, *i.e.* it's an RDKit UFF optimziation if job is None: ret[name].update(uff_fallback) # type: ignore continue settings = Settings(settings) if self.read_template: # Update the settings using a QMFlows template template = qmflows.geometry['specific'][self.type_to_string(job)].copy() settings.soft_update(template) ret[name]['key'] = job ret[name]['value'] = settings return ret
def read_mol_txt(mol_dict: Settings) -> Optional[List[Molecule]]: """Read a plain text file containing one or more SMILES strings.""" try: row = 0 if 'row' not in mol_dict else mol_dict.row column = 0 if 'column' not in mol_dict else mol_dict.column mol_type = 'input_cores' if mol_dict.is_core else 'input_ligands' with open(mol_dict.mol, 'r') as f: iterator = itertools.islice(f, row, None) _file_list = [ i.rstrip('\n').split()[column] for i in iterator if i ] optional_dict = Settings( {k: v for k, v in mol_dict.items() if k not in ('mol', 'path')}) file_list = [{i: optional_dict} for i in _file_list] validate_mol(file_list, mol_type, mol_dict.path) return read_mol(file_list) except Exception as ex: print_exception('read_mol_txt', mol_dict.name, ex)
def pre_process_settings(mol: Molecule, s: Settings, job_type: Type[Job], template_name: str) -> Settings: """Update all :class:`Settings`, **s**, with those from a QMFlows template (see **job**).""" ret = Settings() type_key = type_to_string(job_type) ret.input = getattr(qmflows, template_name)['specific'][type_key].copy() ret.update(s) if job_type is AMSJob: mol.properties.pop('charge', None) # ret.input.ams.system.BondOrders._1 = adf_connectivity(mol) if 'uff' not in s.input: ret.input.ams.system.charge = int( sum(at.properties.get('charge', 0) for at in mol)) elif job_type is ADFJob: mol.properties.pop('charge', None) if not ret.input.charge: ret.input.charge = int( sum(at.properties.get('charge', 0) for at in mol)) return ret
def overlay_cp2k_settings(self, cp2k_settings: MutableMapping, psf: Optional[PSFContainer] = None) -> None: r"""Overlay **df** with all :math:`q`, :math:`\sigma` and :math:`\varepsilon` values from **cp2k_settings**.""" # noqa charge = cp2k_settings['input']['force_eval']['mm']['forcefield'][ 'charge'] charge_dict = { block['atom']: float(block['charge']) for block in charge } if psf is not None: psf_charge_dict = dict(zip(psf.atom_type, psf.charge)) for k, v in psf_charge_dict.items(): if k not in charge_dict: charge_dict[k] = v epsilon_s = Settings() # type: ignore[var-annotated] sigma_s = Settings() # type: ignore[var-annotated] # Check if the settings are qmflows-style generic settings lj = cp2k_settings.get('lennard-jones') or cp2k_settings.get( 'lennard_jones') if lj is not None: self._overlay_s_qmflows(cp2k_settings, sigma_s, epsilon_s) else: lj = cp2k_settings['input']['force_eval']['mm']['forcefield'][ 'nonbonded']['lennard-jones'] # noqa self._overlay_s_plams(lj, sigma_s, epsilon_s) self.set_charge(charge_dict) for unit, dct in epsilon_s.items(): self.set_epsilon_pairs(dct, unit=unit) for unit, dct in sigma_s.items(): self.set_sigma_pairs(dct, unit=unit)
def overlay_cp2k_settings(self, cp2k_settings: Settings) -> None: """Extract forcefield information from PLAMS-style CP2K settings. Performs an inplace update of this instance. Examples -------- Example input value for **cp2k_settings**. In the provided example the **cp2k_settings** are directly extracted from a CP2K .inp file. .. code:: python >>> import cp2kparser # https://github.com/nlesc-nano/CP2K-Parser >>> filename = str(...) >>> cp2k_settings: dict = cp2kparser.read_input(filename) >>> print(cp2k_settings) {'force_eval': {'mm': {'forcefield': {'nonbonded': {'lennard-jones': [...]}}}}} Parameters ---------- cp2k_settings : :class:`~collections.abc.Mapping` A Mapping with PLAMS-style CP2K settings. """ if 'input' not in cp2k_settings: cp2k_settings = Settings({'input': cp2k_settings}) # If cp2k_settings is a Settings instance enable the `suppress_missing` context manager # In this manner normal KeyErrors will be raised, just like with dict if isinstance(cp2k_settings, Settings): context_manager = cp2k_settings.suppress_missing else: context_manager = nullcontext with context_manager(): for prm_map in self.CP2K_TO_PRM.values(): name = prm_map['name'] columns = list(prm_map['columns']) key_path = prm_map['key_path'] key = prm_map['key'] unit = prm_map['unit'] default_unit = prm_map['default_unit'] post_process = prm_map['post_process'] self._overlay_cp2k_settings(cp2k_settings, name, columns, key_path, key, unit, default_unit, post_process)
def extract_args(args: Optional[List[str]] = None) -> Settings: """Extract and return all arguments.""" input_file = args.YAML[0] if exists(input_file): pass elif exists(join(getcwd(), input_file)): input_file = join(getcwd(), input_file) else: input_file2 = join(getcwd(), input_file) raise FileNotFoundError( f'No file found at {input_file} or {input_file2}') with open(input_file, 'r') as file: return Settings(yaml.load(file, Loader=yaml.FullLoader))
def update_ff_jobs(s: Settings) -> None: """Update forcefield settings.""" if NANO_CAT is not None: raise NANO_CAT ff = Settings() set_cp2k_param(ff, s.optional.forcefield) optimize = s.optional.qd.optimize if optimize and optimize.use_ff: if optimize.job1 and str(optimize.job1) == str(Cp2kJob): optimize.s1 = Settings() if optimize.s1 is None else optimize.s1 optimize.s1 += get_template('qd.yaml')['CP2K_CHARM_opt'] optimize.s1 += ff if optimize.job2 and str(optimize.job1) == str(Cp2kJob): optimize.s2 = Settings() if optimize.s2 is None else optimize.s2 optimize.s2 += get_template('qd.yaml')['CP2K_CHARM_opt'] optimize.s2 += ff dissociate = s.optional.qd.dissociate if dissociate and dissociate.use_ff: if dissociate.job1 and str(dissociate.job1) == str(Cp2kJob): dissociate.s1 = Settings( ) if dissociate.s1 is None else dissociate.s1 dissociate.s1 += get_template('qd.yaml')['CP2K_CHARM_opt'] dissociate.s1 += ff activation_strain = s.optional.qd.activation_strain if activation_strain and activation_strain.use_ff: if activation_strain.job1 and str( activation_strain.job1) == str(Cp2kJob): key = 'CP2K_CHARM_singlepoint' if not activation_strain.md else 'CP2K_CHARM_md' activation_strain.s1 = Settings( ) if activation_strain.s1 is None else activation_strain.s1 # noqa activation_strain.s1 += get_template('qd.yaml')[key] activation_strain.s1 += ff
def run_cdft_job(mol: Molecule, job: Type[ADFJob], s: Settings) -> pd.Series: """Run a conceptual DFT job and extract & return all global descriptors.""" results = mol.job_single_point(job, s.copy(), name='CDFT', ret_results=True, read_template=False) if results.job.status in {'crashed', 'failed'}: return _BACKUP ret = get_global_descriptors(results) ret.index = pd.MultiIndex.from_product([['cdft'], ret.index], names=['index', 'sub index']) return ret
def DFT(): s = Settings() s.input.ams.task = 'GeometryOptimization' s.input.adf.basis.type = 'TZ2P' s.input.adf.basis.core = 'None' s.input.adf.xc.hybrid = 'B3LYP' s.input.adf.xc.Dispersion = 'GRIMME3 BJDAMP' s.input.adf.Relativity.Level = 'None' s.input.adf.NumericalQuality = 'Good' s.input.adf.Symmetry = 'NOSYM' s.input.ams.UseSymmetry = 'No' s.input.adf.Unrestricted = 'No' s.input.adf.SpinPolarization = 0 s.input.ams.System.Charge = 0 return s
def _get_logp(s: Settings, name: str, logger: logging.Logger) -> float: logp_s = s.copy() logp_s.update(get_template('qd.yaml')['COSMO-RS logp']) for v in logp_s.input.compound: v._h = v._h.format(os.environ["ADFRESOURCES"]) logp_job = CRSJob(settings=logp_s, name='LogP') results = _crs_run(logp_job, name) try: logp = results.readkf('LOGP', 'logp')[0] logger.info(f'{results.job.__class__.__name__}: {name} LogP ' f'calculation ({results.job.name}) is successful') except Exception: logger.error(f'{results.job.__class__.__name__}: {name} LogP ' f'calculation ({results.job.name}) has failed') logp = np.nan return logp
def start_crs_jobs(mol_list: Iterable[Molecule], jobs: Iterable[Type[Job]], settings: Iterable[Settings], **kwargs: Any) -> List[pd.Series]: # Parse the job type job, *_ = jobs if job is not ADFJob: raise NotImplementedError(f"job: {job.__class__.__name__} = {job!r}") # Parse and update the input settings _s, *_ = settings s = Settings(_s) s.input += cdft.specific.adf ret = [] for mol in mol_list: ret.append(run_cdft_job(mol, job, s)) return ret
def _get_logp(solutes: _NDArray[np.object_]) -> _NDArray[np.float64]: """Perform a LogP calculation.""" ret = np.full_like(solutes, np.nan, dtype=np.float64) mask = solutes != None count = np.count_nonzero(mask) if count == 0: return ret s = copy.deepcopy(LOGP_SETTINGS) for v in s.input.compound[:2]: v._h = v._h.format(os.environ["AMSRESOURCES"]) s.input.compound += [Settings({"_h": f'"{sol}"', "_1": "compkffile"}) for sol in solutes[mask]] ret[mask] = _run_crs( s, count, logp=lambda r: r.readkf('LOGP', 'logp')[2:], ) return ret
def finalize_lj(mol: Molecule, s: List[Settings]) -> None: """Assign UFF Lennard-Jones parameters to all missing non-bonded core/ligand interactions. .. _LENNARD_JONES: https://manual.cp2k.org/trunk/CP2K_INPUT/FORCE_EVAL/MM/FORCEFIELD/NONBONDED/LENNARD-JONES.html Parameters ---------- mol : |plams.Molecule|_ A PLAMS molecule containing a core and ligand(s). s : |list|_ [|plams.Settings|_] A list of settings constructed from the CP2K FORCE_EVAL/MM/FORCEFIELD/NONBONDED/`LENNARD-JONES`_ block. The settings are expected to contain the ``"atoms"`` keys. """ # noqa # Create a set of all core atom types core_at, lig_at = _gather_core_lig_symbols(mol) # Create a set of all user-specified core/ligand LJ pairs if not s: s = [] elif isinstance(s, dict): s = [s] atom_pairs = {frozenset(s.atoms.split()) for s in s} # Check if LJ parameters are present for all atom pairs. # If not, supplement them with UFF parameters. for at1, symbol1 in core_at.items(): for at2, symbol2 in lig_at.items(): at1_at2 = {at1, at2} if at1_at2 in atom_pairs: continue s.append( Settings({ 'atoms': f'{at1} {at2}', 'epsilon': f'[kcalmol] {round(combine_epsilon(symbol1, symbol2), 4)}', 'sigma': f'[angstrom] {round(combine_sigma(symbol1, symbol2), 4)}' }))