diff --git a/.github/workflows/pythonapp.yml b/.github/workflows/pythonapp.yml index 04c0436daf..33d030811a 100644 --- a/.github/workflows/pythonapp.yml +++ b/.github/workflows/pythonapp.yml @@ -7,8 +7,6 @@ on: - "examples/**" - "scripts/**" - "nix/**" - - "tests_staging/**" - - "deprecated/**" workflow_dispatch: schedule: - cron: '0 */12 * * *' @@ -16,12 +14,12 @@ jobs: buildNix: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: cachix/install-nix-action@v22 + - uses: actions/checkout@v4 + - uses: cachix/install-nix-action@v26 with: nix_path: nixpkgs=channel:nixpkgs-unstable extra_nix_config: access-tokens = github.com=${{ secrets.GITHUB_TOKEN }} - - uses: cachix/cachix-action@v12 + - uses: cachix/cachix-action@v14 with: name: pysisyphus authToken: '${{ secrets.CACHIX_AUTH_TOKEN }}' @@ -32,15 +30,15 @@ jobs: build: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/cache@v3 + - uses: actions/checkout@v4 + - uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip- - - name: Set up Python 3.9 - uses: actions/setup-python@v4 + - name: Set up Python 3.12 + uses: actions/setup-python@v5 with: - python-version: 3.9.15 + python-version: "3.12" - name: Upgrade pip run: | python -m pip install --upgrade pip @@ -51,9 +49,9 @@ jobs: run: | pip install flake8 # stop the build if there are Python syntax errors or undefined names - flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics + flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics --config flake8.ini # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide - flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics + flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics --config flake8.ini - name: Install thermoanalysis run: | pip install git+https://github.com/eljost/thermoanalysis.git @@ -64,12 +62,15 @@ jobs: run: > pytest -v --cov=pysisyphus --cov-config .coveragerc --cov-report xml --cov-report term + --color=yes --show-capture=no --durations=0 -m "not benchmark and not skip_ci" tests - name: Upload coverage to codecov - uses: codecov/codecov-action@v3 + uses: codecov/codecov-action@v4 with: file: ./coverage.xml flags: unittests + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 6e45808d7f..ab6d45cad6 100644 --- a/.gitignore +++ b/.gitignore @@ -169,6 +169,13 @@ pysisyphus/wavefunction/devel_ints/* version.py !tests/test_barriers/*.h5 +!tests/test_diabatization/00_bena2_cat/00_bena2_dcat.bson +!tests/test_diabatization/00_bena2_cat/00_bena2_dcat.cis +!tests/test_diabatization/00_bena2_cat/00_bena2_dcat.log +!tests/test_dma/data/* +!tests/test_es_capabilities/control_path_mult_* +!tests/test_exc_densities/data/* +!tests/test_rates/ethyl_propionate/*.h5 !tests/test_franckcondon/*.h5 !tests/test_franckcondon/naphthalene_gradient !tests/test_hessian_updates/*.h5 @@ -182,12 +189,18 @@ version.py !tests/test_orca/orca_tddft_triplets.out !tests/test_orca/fail_term.orca.out !tests/test_orca/orca_tddft_triplets.out +!tests/test_orca/restricted.gbw +!tests/test_orca/unrestricted.gbw +!tests/test_openmolcas/butadien_vdzp.rasscf.h5 !tests/test_openmolcas/cms_pdft_opt.yaml -!tests/test_openmolcas/cms_pdft_opt.RasOrb +!tests/test_openmolcas/cms_pdft.rasscf.h5 !tests/test_point_charges/methane_control_path/* !tests/test_xtb/xtb_pass.out !tests/test_xtb/xtb_crash.out +!tests/test_thermo/irc_*.orca.h5 +!tests/test_thermo/hcn_orca_b973c_hessian.h5 !tests/test_turbomole/control_path_big_hess/* +!tests/test_turbomole/make_dens/*/* !tests/test_tsopt/inp_hessian_ref.h5 !tests/test_rates/peroxyl_r1_h5s/*.h5 !tests/test_integrals/methane_def2svp_aomix.in @@ -197,5 +210,12 @@ version.py !tests/test_wavefunction/pyrrole/* !tests/test_psf/step1_pdbreader.psf !tests/test_overlap_calculator/benzene/* +!tests/test_local_force_constants/bf3_ccsd_t_ccpvdz.h5 !tests/test_wavefunction/bases/* +!tests/test_so_coupling/data/* +!tests/test_turbomole/make_dens/* +!tests/test_numerov/*.dat +!tests/test_partfuncs/ts_final_hessian_xtb.h5 !pysisyphus/wf_library/* +!pysisyphus/numint/lebedev_grids_4pi.npz +!pysisyphus/test_cos/multi_state_inp.npz diff --git a/README.md b/README.md index b66200e342..f4d596f4fe 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -![pysisyphus logo](resources/logo_small.png) +![pysisyphus logo](resources/logo_new_cut_small.png) [![Documentation Status](https://readthedocs.org/projects/pysisyphus/badge/?version=master)](https://pysisyphus.readthedocs.io/en/master/?badge=master) [![build](https://github.com/eljost/pysisyphus/workflows/Python%20application/badge.svg)](https://github.com/eljost/pysisyphus/actions) diff --git a/deprecated/AnimGS.py b/deprecated/AnimGS.py deleted file mode 100644 index e1e2d5b81e..0000000000 --- a/deprecated/AnimGS.py +++ /dev/null @@ -1,51 +0,0 @@ -#!/usr/bin/env python3 - -import matplotlib.pyplot as plt -import matplotlib.animation as animation -import numpy as np - -class AnimGS: - - def __init__(self, gs, calc_getter): - self.gs = gs - self.calc = calc_getter() - - self.calc.plot() - self.fig = self.calc.fig - self.ax = self.calc.ax - - self.coords = [c.reshape(-1, 3) for c in self.gs.coords_list] - self.tangents = self.gs.tangent_list - self.perp_forces = self.gs.perp_force_list - - c0x = self.coords[0][:,0] - c0y = self.coords[0][:,1] - self.coord_lines, = self.ax.plot(c0x, c0y, "X-", ms=8, c="k") - t0x = self.tangents[0][:,0] - t0y = self.tangents[0][:,1] - self.tang_quiv = self.ax.quiver(c0x, c0y, t0x, t0y) - - def update_func(self, frame): - self.fig.suptitle(f"Cycle {frame}") - - coords = self.coords[frame] - cx = self.coords[frame][:,0] - cy = self.coords[frame][:,1] - self.coord_lines.set_xdata(cx) - self.coord_lines.set_ydata(cy) - - offsets = np.stack((cx, cy), axis=-1).flatten() - - tx = self.tangents[frame][:,0] - ty = self.tangents[frame][:,1] - self.tang_quiv.set_offsets(offsets) - self.tang_quiv.set_UVC(tx, ty) - - def animate(self): - frames = range(self.gs.cur_cycle) - self.animation = animation.FuncAnimation( - self.fig, - self.update_func, - frames=frames, - interval=250, - ) diff --git a/deprecated/GSref.py b/deprecated/GSref.py deleted file mode 100644 index ae90874afb..0000000000 --- a/deprecated/GSref.py +++ /dev/null @@ -1,152 +0,0 @@ -#/!usr/bin/env python3 - -# See [1] 10.1063/1.1691018 - -import numpy as np -from scipy.interpolate import splprep, splev - -from pysisyphus.cos.GrowingChainOfStates import GrowingChainOfStates - - -class GrowingString(GrowingChainOfStates): - - def __init__(self, images, calc_getter, max_cycles=75, **kwargs): - assert len(images) >= 2, "Need at least 2 images for GrowingString." - if len(images) >= 2: - images = [images[0], images[-1]] - print("More than 2 images were supplied! Will only use the " - "first and last images to start the GrowingString." - ) - super().__init__(images, calc_getter, **kwargs) - - self.max_cycles = max_cycles - left_img, right_img = self.images - - self.left_string = [left_img, ] - self.right_string = [right_img, ] - - self.tangent_list = list() - self.perp_force_list = list() - self.coords_list = list() - - @property - def left_size(self): - return len(self.left_string) - - @property - def right_size(self): - return len(self.right_string) - - @property - def string_size(self): - return self.left_size + self.right_size - - @property - def images_left(self): - """Returns wether we already created all images.""" - return (self.left_size-1 + self.right_size-1) < self.max_nodes - - @property - def lf_ind(self): - """Index of the left frontier node in self.images.""" - return len(self.left_string)-1 - - @property - def rf_ind(self): - """Index of the right frontier node in self.images.""" - return self.lf_ind+1 - - def spline(self): - reshaped = self.coords.reshape(-1, self.coords_length) - # To use splprep we have to transpose the coords. - transp_coords = reshaped.transpose() - tcks, us = zip(*[splprep(transp_coords[i:i+9], s=0, k=3) - for i in range(0, len(transp_coords), 9)] - ) - return tcks, us - - def reparam(self, tcks, param_density): - # Reparametrize mesh - new_points = np.vstack([splev(param_density, tck) for tck in tcks]) - # Flatten along first dimension. - new_points = new_points.reshape(-1, len(self.images)) - self.coords = new_points.transpose().flatten() - - def run(self): - # To add nodes we need a direction/tangent. As we start - # from two images we can't do a cubic spline yet, so we - # can't use splined tangents. - # To start off we use a "classic" tangent instead, that - # is the unit vector pointing from the left image, to - # the right image. - init_tangent = super().get_tangent(0) - Sk, _ = self.arc_dims - S = Sk / (self.max_nodes+1) - # Create first two mobile nodes - new_left_coords = S*init_tangent - new_right_coords = - S*init_tangent - left_frontier = self.get_new_image(new_left_coords, 1, 0) - self.left_string.append(left_frontier) - right_frontier = self.get_new_image(new_right_coords, 2, 2) - self.right_string.append(right_frontier) - - def ind_func(perp_force, tol=0.5): # lgtm [py/unused-local-variable] - return int(np.linalg.norm(perp_force) <= tol) - - # Step length on the normalized arclength - sk = 1 / (self.max_nodes+1) # lgtm [py/unused-local-variable] - tcks, us = self.spline() - for self.cur_cycle in range(self.max_cycles): - Sk, cur_mesh = self.arc_dims - print(f"Cycle {self.cur_cycle:03d}, total arclength Sk={Sk:.4f}") - forces = self.forces - self.cur_forces = forces.reshape(-1, 3) - tangents = np.vstack([splev(cur_mesh, tck, der=1) for tck in tcks]).T - norms = np.linalg.norm(tangents, axis=1) - tangents = tangents / norms[:,None] - # Tangents of the right string shall point towards the center. - tangents[self.rf_ind:] *= -1 - self.tangents = tangents - self.tangent_list.append(self.tangents) - - # Perpendicular force component - # Dot product between rows in one line - # np.einsum("ij,ij->i", tangents, forces.reshape(-1, 3)) - force_per_img = forces.reshape(-1, 3) - self.perp_forces = np.array( - [force - force.dot(tangent)*tangent - for force, tangent in zip(force_per_img, tangents)] - ) - self.perp_force_list.append(self.perp_forces) - np.save("gs_ref_perp.npy", self.perp_forces) - - # Take step - step = 0.05 * self.perp_forces.flatten() - step_norms = np.linalg.norm(step.reshape(-1, 3), axis=1) - self.coords += step - self.coords_list.append(self.coords) - # Spline displaced coordinates - tcks, us = self.spline() - - # Check if we can add new nodes - if self.images_left and ind_func(self.perp_forces[self.lf_ind]): - # Insert at the end of the left string, just before the - # right frontier node. - new_left_frontier = self.get_new_image(self.zero_step, self.rf_ind, self.lf_ind) - self.left_string.append(new_left_frontier) - print("Added new left frontier node.") - if self.images_left and ind_func(self.perp_forces[self.rf_ind]): - # Insert at the end of the right string, just before the - # current right frontier node. - new_right_frontier = self.get_new_image(self.zero_step, self.rf_ind, self.rf_ind) - self.right_string.append(new_right_frontier) - print("Added new right frontier node.") - - print("Current string size:", self.left_size, "+", self.right_size) - # Reparametrize nodes - left_inds = np.arange(self.left_size) - right_inds = np.arange(self.max_nodes+2)[-self.right_size:] - param_inds = np.concatenate((left_inds, right_inds)) - param_density = sk*param_inds - print("New param density", np.array2string(param_density, precision=2)) - self.reparam(tcks, param_density) diff --git a/deprecated/MullerBrownSympyPot2D.py b/deprecated/MullerBrownSympyPot2D.py deleted file mode 100644 index 5dc3dd0f1b..0000000000 --- a/deprecated/MullerBrownSympyPot2D.py +++ /dev/null @@ -1,36 +0,0 @@ -from pysisyphus.calculators.AnaPotBase2D import AnaPotBase2D - -# [1] http://www.cims.nyu.edu/~eve2/string_jcp_simplified07.pdf - -class MullerBrownSympyPot2D(AnaPotBase2D): - - def __init__(self): - A = (-200, -100, -170, 15) - x0 = (1.0, 0.0, -0.5, -1.0) - y0 = (0.0, 0.5, 1.5, 1.0) - a = (-1.0, -1.0, -6.5, 0.7) - b = (0.0, 0.0, 11.0, 0.6) - c = (-10.0, -10.0, -6.5, 0.7) - - #V_str_base = """{Ai} * exp( - # {ai}*(x-{xi})**2 - # + {bi}*(x-{xi})*(y-{yi}) - # + {ci}*(y-{yi})**2 - #)""" - V_str_base = "{Ai}*exp({ai}*(x-{xi})**2 + {bi}*(x-{xi})*(y-{yi}) + {ci}*(y-{yi})**2)" - V_str = "" - V_strs = [V_str_base.format( - Ai=A[i], - ai=a[i], - xi=x0[i], - bi=b[i], - yi=y0[i], - ci=c[i]) - for i in range(4) - ] - V_str = " + ".join(V_strs) - - super(MullerBrownSympyPot2D, self).__init__(V_str=V_str) - - def __str__(self): - return "MullerBrownSympyPot2D calculator" diff --git a/deprecated/NoSpringNeb.py b/deprecated/NoSpringNeb.py deleted file mode 100644 index 0bf2f05bcb..0000000000 --- a/deprecated/NoSpringNeb.py +++ /dev/null @@ -1,17 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np - -from pysisyphus.cos.NEB import NEB - -class NoSpringNEB(NEB): - - #def __init__(self, calculator, images): - def __init__(self, calculator, images): - super(NoSpringNEB, self).__init__(calculator, images) - - def get_perpendicular_force(self, i): - return np.array(self.calculator.get_grad(*self.images[i])) - - def get_parallel_force(self, i): - return (0, 0) diff --git a/deprecated/ONIOM.py b/deprecated/ONIOM.py deleted file mode 100644 index 77cb46435c..0000000000 --- a/deprecated/ONIOM.py +++ /dev/null @@ -1,169 +0,0 @@ -#!/usr/bin/env python3 - -import itertools as it -from collections import namedtuple - -import numpy as np -from scipy.spatial.distance import pdist - -# from pysisyphus.calculators import * -from pysisyphus.calculators import Gaussian16, OpenMolcas, ORCA, Psi4, Turbomole, XTB -from pysisyphus.calculators.Calculator import Calculator -from pysisyphus.Geometry import Geometry -from pysisyphus.elem_data import COVALENT_RADII as CR - - -CALC_DICT = { - # "g09": Gaussian09.Gaussian09, - "g16": Gaussian16, - "openmolcas": OpenMolcas.OpenMolcas, - "orca": ORCA, - "psi4": Psi4, - "turbomole": Turbomole, - "xtb": XTB, - # "pyscf": PySCF, - # "pypsi4": PyPsi4, - # "pyxtb": PyXTB, -} - - -LinkMap = namedtuple("LinkMap", "r1_ind r3_ind g") - - -def get_cov_radii_sum_array(atoms, coords): - coords3d = coords.reshape(-1, 3) - atom_indices = list(it.combinations(range(len(coords3d)),2)) - atom_indices = np.array(atom_indices, dtype=int) - cov_rad_sums = list() - for i, j in atom_indices: - atom1 = atoms[i].lower() - atom2 = atoms[j].lower() - cov_rad_sums.append(CR[atom1] + CR[atom2]) - cov_rad_sums = np.array(cov_rad_sums) - return cov_rad_sums - - -def get_bond_sets(atoms, coords3d, bond_factor=1.3): - cdm = pdist(coords3d) - # Generate indices corresponding to the atom pairs in the - # condensed distance matrix cdm. - atom_indices = list(it.combinations(range(len(coords3d)),2)) - atom_indices = np.array(atom_indices, dtype=int) - cov_rad_sums = get_cov_radii_sum_array(atoms, coords3d.flatten()) - cov_rad_sums *= bond_factor - bond_flags = cdm <= cov_rad_sums - bond_indices = atom_indices[bond_flags] - return bond_indices - - -def cap(geom, high_frag): - high_set = set(high_frag) - ind_set = set(range(len(geom.atoms))) - rest = ind_set - high_set - - # Determine bond(s) that connect high_frag with the rest - # bonds, _, _ = geom.internal.prim_indices - bonds = get_bond_sets(geom.atoms, geom.coords3d) - bond_sets = [set(b) for b in bonds] - - # Find all bonds that involve one atom of model. These bonds - # connect the model to the real geometry. We want to cap these - # bonds. - break_bonds = [b for b in bond_sets if len(b & high_set) == 1] - - # Put capping atoms at every bond to break. - # The model fragment size corresponds to the length of the union of - # the model set and the atoms in break_bonds. - capped_frag = high_set.union(*break_bonds) - capped_inds = list(sorted(capped_frag)) - - # Index map between the new model geometry and the original indices - # in the real geometry. - atom_map = {model_ind: real_ind for real_ind, model_ind - in zip(capped_inds, range(len(capped_inds)))} - - # g = 0.723886 # Gaussian16 - g = 0.709 # Paper g98-ONIOM-implementation - c3d = geom.coords3d.copy() - new_atoms = list(geom.atoms) - link_maps = dict() - for bb in break_bonds: - to_cap = bb - high_set - assert len(to_cap) == 1 - r1_ind = list(bb - to_cap)[0] - r3_ind = tuple(to_cap)[0] - r1 = c3d[r1_ind] - r3 = c3d[r3_ind] - r2 = r1 + g*(r3-r1) - c3d[r3_ind] = r2 - new_atoms[r3_ind] = "H" - new_ind = np.sum(np.array(high_frag) < r3_ind) - link_map = LinkMap(r1_ind=r1_ind, r3_ind=r3_ind, g=g) - link_maps[new_ind] = link_map - - capped_atoms = [new_atoms[i] for i in capped_inds] - capped_coords = c3d[capped_inds].flatten() - capped_geom = Geometry(capped_atoms, capped_coords) - - return capped_geom, atom_map, link_maps - - -class ONIOM(Calculator): - - def __init__(self, calc_dict, model_inds): - super().__init__() - - self.calc_dict = calc_dict - self.model_inds = model_inds - - high_level = self.calc_dict["high"] - high_type = high_level.pop("type") - high_cls = CALC_DICT[high_type] - - low_level = self.calc_dict["low"] - low_type = low_level.pop("type") - low_cls = CALC_DICT[low_type] - - self.model_high_calc = high_cls(calc_number=0, **high_level) - self.real_low_calc = low_cls(calc_number=1, **low_level) - self.model_low_calc = low_cls(calc_number=2, **low_level) - - def get_forces(self, atoms, coords): - tmp_geom = Geometry(atoms, coords) - results = self.oniom2(tmp_geom) - return results - - def oniom2(self, real_geom): - model_geom, atom_map, links = cap(real_geom, self.model_inds) - - results_3 = self.real_low_calc.get_forces(real_geom.atoms, real_geom.cart_coords) - results_1 = self.model_low_calc.get_forces(model_geom.atoms, model_geom.cart_coords) - results_2 = self.model_high_calc.get_forces(model_geom.atoms, model_geom.cart_coords) - - E3 = results_3["energy"] - E1 = results_1["energy"] - E2 = results_2["energy"] - g3 = -results_3["forces"].reshape(-1, 3) - g1 = -results_1["forces"].reshape(-1, 3) - g2 = -results_2["forces"].reshape(-1, 3) - - # ONIOM energy - energy_oniom = E3 - E1 + E2 - - # ONIOM gradient - gradient_oniom = g3 - for model_ind, real_ind in atom_map.items(): - qm_correction = -g1[model_ind] + g2[model_ind] - if model_ind in links: - model_sum = -g1[model_ind] + g3[model_ind] - r1_ind, r3_ind, g = links[model_ind] - gradient_oniom[r1_ind] += (1-g) * qm_correction - gradient_oniom[r3_ind] += g * qm_correction - else: - gradient_oniom[real_ind] += qm_correction - - results = { - "energy": energy_oniom, - "forces": -gradient_oniom.flatten(), - } - return results diff --git a/deprecated/OldTangentChainOfStates.py b/deprecated/OldTangentChainOfStates.py deleted file mode 100644 index 868ef52127..0000000000 --- a/deprecated/OldTangentChainOfStates.py +++ /dev/null @@ -1,108 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np - -from pysisyphus.Geometry import Geometry -from pysisyphus.xyzloader import make_trj_str - -class ChainOfStates: - - def __init__(self, images): - self.images = images - - self._coords = None - self._forces = None - self.coords_length = self.images[0].coords.size - - @property - def coords(self): - """Return a flat 1d array containing the coordinates of all images.""" - all_coords = [image.coords for image in self.images] - self._coords = np.concatenate(all_coords) - return self._coords - - @coords.setter - def coords(self, coords): - """Distribute the flat 1d coords array over all images.""" - coords = coords.reshape(-1, self.coords_length) - for image, c in zip(self.images, coords): - image.coords = c - - @property - def energy(self): - self._energy = [image.energy for image in self.images] - return self._energy - - @property - def forces(self): - forces = [image.forces for image in self.images] - self._forces = np.concatenate(forces) - return self._forces - - @forces.setter - def forces(self, forces): - forces = forces.reshape(-1, self.coords_length) - for image, f in zip(self.images, forces): - image.forces = f - - @property - def perpendicular_forces(self): - indices = range(len(self.images)) - perp_forces = [self.get_perpendicular_forces(i) for i in indices] - return np.array(perp_forces).flatten() - - def interpolate_between(self, initial_ind, final_ind, image_num): - initial_coords = self.images[initial_ind].coords - final_coords = self.images[final_ind].coords - step = (final_coords-initial_coords) / (image_num+1) - # initial + i*step - i_array = np.arange(1, image_num+1) - atoms = self.images[0].atoms - new_coords = initial_coords + i_array[:, None]*step - return [Geometry(atoms, nc) for nc in new_coords] - - def interpolate(self, image_num=5): - new_images = list() - # Iterate over image pairs (i, i+1) and interpolate between them - for i in range(len(self.images)-1): - interpol_images = self.interpolate_between(i, i+1, image_num) - new_images.append(self.images[i]) - new_images.extend(interpol_images) - # As we only added the i-th image and the new images we have to add - # the last (i+1)-th image at the end. - new_images.append(self.images[-1]) - self.images = new_images - - def fix_ends(self): - zero_forces = np.zeros_like(self.images[0].coords) - self.images[0].forces = zero_forces - self.images[-1].forces = zero_forces - - def get_tangent(self, i): - # Use a one-sided difference for the first and last image - if i == 0: - prev_index = i - next_index = 1 - elif i == (len(self.images) - 1): - prev_index = i - 1 - next_index = len(self.images) - 1 - # If i is an inner index use the image before and after i - else: - prev_index = i - 1 - next_index = i + 1 - # [1], Eq. (2) - prev_image = self.images[prev_index].coords - next_image = self.images[next_index].coords - return (next_image-prev_image) / np.linalg.norm(next_image-prev_image) - - def get_perpendicular_forces(self, i): - forces = self.images[i].forces - tangent = self.get_tangent(i) - return forces - (np.vdot(forces, tangent)*tangent) - - def save(self, out_fn): - atoms = self.images[0].atoms - coords_list = [image.coords.reshape((-1,3)) for image in self.images] - trj_str = make_trj_str(atoms, coords_list) - with open(out_fn, "w") as handle: - handle.write(trj_str) diff --git a/deprecated/PlotAnaPot.py b/deprecated/PlotAnaPot.py deleted file mode 100644 index a22e9eb212..0000000000 --- a/deprecated/PlotAnaPot.py +++ /dev/null @@ -1,38 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np - -class PlotAnaPot: - - def __init__(self, geometry, xlim, ylim, levels): - self.geometry = geometry - self.xlim = xlim - self.ylim = ylim - self.levels = levels - - self.fig, self.ax = plt.subplots(figsize=(8,8)) - - x = np.linspace(*self.xlim, 100) - y = np.linspace(*self.ylim, 100) - X, Y = np.meshgrid(x, y) - atoms = self.geometry.atoms - pot_coords = np.stack((X, Y)) - pot = self.geometry.calculator.get_energy(atoms, pot_coords)["energy"] - levels = np.linspace(*self.levels) - contours = self.ax.contour(X, Y, pot, levels) - - def plot(self, coords): - self.ax.plot(*zip(*coords), "ro", ls="-") - - def plot_gs(self, coords, pivot_coords, micro_coords): - # Pivot points - self.ax.plot(*zip(*pivot_coords), "bo", ls="-", label="pivot") - # Constrained optmizations - for mc in micro_coords: - self.ax.plot(*zip(*mc), "yo", ls="-") - for i, m in enumerate(mc): - self.ax.text(*m, str(i)) - self.plot(coords) - plt.legend() - - def show(self): - plt.show() diff --git a/deprecated/ReferenceNEB.py b/deprecated/ReferenceNEB.py deleted file mode 100755 index 84db13636a..0000000000 --- a/deprecated/ReferenceNEB.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python3 - -import matplotlib.pyplot as plt -import matplotlib.animation as animation -import numpy as np -from sympy import symbols, diff, lambdify, sympify - - -class AnimPlot: - - def __init__(self, coords, tangents, cycles, ge): - self.coords = coords - self.tangents = tangents - print("tangents hsape", tangents.shape) - self.cycles = cycles - self.ge = ge - - self.fig, self.ax = plt.subplots(figsize=(8,8)) - self.pause = True - self.fig.canvas.mpl_connect('key_press_event', self.on_keypress) - - xlim = (-2, 2.5) - ylim = (0, 5) - x = np.linspace(*xlim, 100) - y = np.linspace(*ylim, 100) - X, Y = np.meshgrid(x, y) - pot_coords = np.stack((X, Y)) - pot = self.ge(pot_coords) - - levels = (-3, 6, 50) - levels = np.linspace(*levels) - contours = self.ax.contour(X, Y, pot, levels) - - images_x = self.coords[0][:,0] - images_y = self.coords[0][:,1] - """ - forces_x = self.forces[0][:,0] - forces_y = self.forces[0][:,1] - """ - tangents_x = self.tangents[0][:,0] - tangents_y = self.tangents[0][:,1] - - self.images, = self.ax.plot(images_x, images_y, "ro", ls="-") - # Tangents - self.tangent_quiv = self.ax.quiver(images_x[1:-1], images_y[1:-1], - tangents_x, tangents_y, color="b") - - - def func(self, frame): - self.fig.suptitle("Cycle {}".format(frame)) - - images_x = self.coords[frame][:,0] - images_y = self.coords[frame][:,1] - self.images.set_xdata(images_x) - self.images.set_ydata(images_y) - - offsets = np.stack((images_x[1:-1], images_y[1:-1]), axis=-1).flatten() - - # Update tangent quiver - tangents_x = self.tangents[frame][:,0] - tangents_y = self.tangents[frame][:,1] - self.tangent_quiv.set_offsets(offsets) - self.tangent_quiv.set_UVC(tangents_x, tangents_y) - - def animate(self): - self.animation = animation.FuncAnimation(self.fig, - self.func, - frames=range(self.cycles), - interval=250) - plt.show() - - def on_keypress(self, event): - """Pause on SPACE press.""" - #https://stackoverflow.com/questions/41557578 - if event.key == " ": - if self.pause: - self.animation.event_source.stop() - else: - self.animation.event_source.start() - self.pause = not self.pause - - -def make_potential(V_str): - x, y = symbols("x y") - V = sympify(V_str) - dVdx = diff(V, x) - dVdy = diff(V, y) - V = lambdify((x, y), V, "numpy") - dVdx = lambdify((x, y), dVdx, "numpy") - dVdy = lambdify((x, y), dVdy, "numpy") - return V, dVdx, dVdy - - -def get_energy(V, coords): - x, y = coords - #x = coords[:, 0] - #y = coords[:, 1] - return V(x, y) - - -def get_forces(dVdx, dVdy, coords): - x, y = coords - dVdx = dVdx(x, y) - dVdy = dVdy(x, y) - return -np.array((dVdx, dVdy)) - - -def interpolate(initial, final, new_images): - step = (final-initial) / (new_images+1) - # initial + i*step - i_array = np.arange(0, new_images+2) - new_coords = initial + i_array[:, None]*step - return np.array(new_coords) - - -def run(): - new_images = 8 - max_cycles = 10 - k = 0.01 - alpha = 0.05 - - #V_str = "(1 - x**2 - y**2)**2 + (y**2) / (x**2 + y**2)" - #initial = np.array((-0.5, 0.5, 0)) - #final = np.array((0.5, 0.5, 0)) - - V_str = "4 + 4.5*x - 4*y + x**2 + 2*y**2-2*x*y + x**4 - 2*x**2*y" - initial = np.array((-1.05274, 1.02776)) - final = np.array((1.94101, 3.85427)) - - V, dVdx, dVdy = make_potential(V_str) - ge = lambda c: get_energy(V, c) - gf = lambda c: get_forces(dVdx, dVdy, c) - - coords = interpolate(initial, final, new_images=new_images) - - all_coords = list() - all_true_forces = list() - all_neb_forces = list() - all_energies = list() - all_tangents = list() - for i in range(max_cycles): - energies = ge(coords.transpose()) - forces = gf(coords.transpose()) - - all_coords.append(coords.copy()) - all_true_forces.append(forces) - all_energies.append(energies) - - neb_forces = list() - tangents = list() - for j in range(1, len(coords)-1): - prev_coords = coords[j-1] - jth_coords = coords[j] - next_coords = coords[j+1] - - prev_energy = energies[j-1] - jth_energy = energies[j] - next_energy = energies[j+1] - if next_energy > jth_energy > prev_energy: - tangent = next_coords - jth_coords - elif next_energy < jth_energy < prev_energy: - tangent = jth_coords - prev_coords - else: - max_energy_diff = max(abs(next_energy-jth_energy), - abs(prev_energy-jth_energy) - ) - min_energy_diff = min(abs(next_energy-jth_energy), - abs(prev_energy-jth_energy) - ) - left_coords_diff = next_coords - jth_coords - right_coords_diff = jth_coords - prev_coords - - if next_energy > prev_energy: - tangent = (left_coords_diff*max_energy_diff - + right_coords_diff*min_energy_diff - ) - else: - tangent = (left_coords_diff*min_energy_diff - + right_coords_diff*max_energy_diff - ) - tangent = tangent / np.linalg.norm(tangent) - tangents.append(tangent) - - parallel_forces = (k * (np.linalg.norm(next_coords-jth_coords) - - np.linalg.norm(prev_coords-jth_coords)) - * tangent - ) - perpendicular_gradient = (-forces[:,j] - + np.dot(forces[:,j], tangent) * tangent - ) - neb_forces.append(parallel_forces-perpendicular_gradient) - neb_forces = np.array(neb_forces) - all_neb_forces.append(neb_forces) - all_tangents.append(tangents) - - steps = alpha*neb_forces - coords[1:-1] += steps - - all_coords = np.array(all_coords) - all_tangents = np.array(all_tangents) - - anim_plot = AnimPlot(all_coords, all_tangents, max_cycles, ge) - anim_plot.animate() - - -if __name__ == "__main__": - run() diff --git a/deprecated/calculators/AnaPot2D.py b/deprecated/calculators/AnaPot2D.py deleted file mode 100644 index 28a3f6eba2..0000000000 --- a/deprecated/calculators/AnaPot2D.py +++ /dev/null @@ -1,10 +0,0 @@ -from pysisyphus.calculators.AnaPotBase2D import AnaPotBase2D - -class AnaPot2D(AnaPotBase2D): - - def __init__(self): - V_str = "4 + 4.5*x - 4*y + x**2 + 2*y**2-2*x*y + x**4 - 2*x**2*y" - super(AnaPot2D, self).__init__(V_str=V_str) - - def __str__(self): - return "AnaPot2D calculator" diff --git a/deprecated/calculators/AnaPotBase2D.py b/deprecated/calculators/AnaPotBase2D.py deleted file mode 100644 index ef254dbb0a..0000000000 --- a/deprecated/calculators/AnaPotBase2D.py +++ /dev/null @@ -1,49 +0,0 @@ -import numpy as np -from sympy import symbols, diff, lambdify, sympify - -from pysisyphus.calculators.Calculator import Calculator - -class AnaPotBase2D(Calculator): - - def __init__(self, V_str): - super(AnaPotBase2D, self).__init__() - x, y = symbols("x y") - V = sympify(V_str) - dVdx = diff(V, x) - dVdy = diff(V, y) - self.V = lambdify((x, y), V, "numpy") - self.dVdx = lambdify((x, y), dVdx, "numpy") - self.dVdy = lambdify((x, y), dVdy, "numpy") - - dVdxdx = diff(V, x, x) - dVdxdy = diff(V, x, y) - dVdydy = diff(V, y, y) - - self.dVdxdx = lambdify((x, y), dVdxdx, "numpy") - self.dVdxdy = lambdify((x, y), dVdxdy, "numpy") - self.dVdydy = lambdify((x, y), dVdydy, "numpy") - - def get_energy(self, atoms, coords): - x, y = coords - energy = self.V(x, y) - return {"energy": energy} - - def get_forces(self, atoms, coords): - x, y = coords - dVdx = self.dVdx(x, y) - dVdy = self.dVdy(x, y) - forces = -np.array((dVdx, dVdy)) - results = self.get_energy(atoms, coords) - results["forces"] = forces - return results - - def get_hessian(self, atoms, coords): - x, y = coords - dVdxdx = self.dVdxdx(x, y) - dVdxdy = self.dVdxdy(x, y) - dVdydy = self.dVdydy(x, y) - hessian = np.array(((dVdxdx, dVdxdy), - (dVdxdy, dVdydy))) - results = self.get_forces(atoms, coords) - results["hessian"] = hessian - return results diff --git a/deprecated/calculators/MullerBrownPot.py b/deprecated/calculators/MullerBrownPot.py deleted file mode 100644 index fe829fdd79..0000000000 --- a/deprecated/calculators/MullerBrownPot.py +++ /dev/null @@ -1,78 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np - -from pysisyphus.calculators.Calculator import Calculator - -class MullerBrownPot(Calculator): - - def __init__(self): - super(MullerBrownPot, self).__init__() - - - def get_energy(self, atoms, coords): - x, y, z = coords - - A = (-200, -100, -170, 15) - x0 = (1.0, 0.0, -0.5, -1.0) - y0 = (0.0, 0.5, 1.5, 1.0) - a = (-1.0, -1.0, -6.5, 0.7) - b = (0.0, 0.0, 11.0, 0.6) - c = (-10.0, -10.0, -6.5, 0.7) - - energy = 0 - for i in range(4): - energy += (A[i] * np.exp( - a[i] * (x - x0[i])**2 + - b[i] * (x - x0[i]) * (y - y0[i]) + - c[i] * (y - y0[i])**2 - ) - ) - return {"energy": energy} - - def get_forces(self, atoms, coords): - x, y, z = coords - A = (-200, -100, -170, 15) - x0 = (1.0, 0.0, -0.5, -1.0) - y0 = (0.0, 0.5, 1.5, 1.0) - a = (-1.0, -1.0, -6.5, 0.7) - b = (0.0, 0.0, 11.0, 0.6) - c = (-10.0, -10.0, -6.5, 0.7) - - forces = 0 - dx = 0 - dy = 0 - for i in range(4): - dx += (A[i] * - (2*a[i]*(x - x0[i]) + b[i]*(y - y0[i])) * - np.exp( - a[i] * (x - x0[i])**2 + - b[i] * (x - x0[i]) * (y - y0[i]) + - c[i] * (y - y0[i])**2 - ) - ) - dy += (A[i] * - (b[i]*(x - x0[i]) + 2*c[i]*(y - y0[i])) * - np.exp( - a[i] * (x - x0[i])**2 + - b[i] * (x - x0[i]) * (y - y0[i]) + - c[i] * (y - y0[i])**2 - ) - ) - dz = np.zeros_like(dx) - forces = -np.stack((dx, dy, dz), axis=-1) - - results = self.get_energy(atoms, coords) - results["forces"] = forces - return results - - """ - def get_hessian(self, atoms, coords): - x, y, z = coords - self._hessian = ((12*x**2 + 2 - 4*y, -4*x-2), - (-4*x-2, 4) - ) - """ - - def __str__(self): - return "Müller-Brown-Potential" diff --git a/deprecated/calculators/OverlapCalculator.py b/deprecated/calculators/OverlapCalculator.py deleted file mode 100644 index ae7a8b66e3..0000000000 --- a/deprecated/calculators/OverlapCalculator.py +++ /dev/null @@ -1,889 +0,0 @@ -# [1] https://pubs.acs.org/doi/pdf/10.1021/acs.jctc.5b01148 -# Plasser, 2016 -# [2] https://doi.org/10.1002/jcc.25800 -# Garcia, Campetella, 2019 - -from collections import namedtuple -from pathlib import Path, PosixPath -import shutil -import tempfile - -import h5py -import numpy as np - -from pysisyphus import logger -from pysisyphus.calculators.Calculator import Calculator -from pysisyphus.calculators.WFOWrapper import WFOWrapper -from pysisyphus.config import get_cmd -from pysisyphus.helpers_pure import describe -from pysisyphus.io.hdf5 import get_h5_group -from pysisyphus.wrapper.mwfn import make_cdd, get_mwfn_exc_str -from pysisyphus.wrapper.jmol import render_cdd_cube as render_cdd_cube_jmol -from pysisyphus.wavefunction.excited_states import ( - rAB as top_rAB, - tden_overlaps, - nto_overlaps, - nto_org_overlaps, - norm_ci_coeffs, -) - - -NTOs = namedtuple("NTOs", "ntos lambdas") - - -def get_data_model( - exc_state_num, occ_mo_num, virt_mo_num, ovlp_type, atoms, max_cycles -): - mo_num = occ_mo_num + virt_mo_num - state_num = exc_state_num + 1 # including GS - _1d = (max_cycles,) - ovlp_state_num = state_num if ovlp_type == "wfow" else exc_state_num - - data_model = { - "mo_coeffs": (max_cycles, mo_num, mo_num), - "ci_coeffs": (max_cycles, exc_state_num, occ_mo_num, virt_mo_num), - "X": (max_cycles, exc_state_num, occ_mo_num, virt_mo_num), - "Y": (max_cycles, exc_state_num, occ_mo_num, virt_mo_num), - "coords": (max_cycles, len(atoms) * 3), - "all_energies": ( - max_cycles, - state_num, - ), - "calculated_roots": _1d, - "roots": _1d, - "root_flips": _1d, - "row_inds": _1d, - "ref_cycles": _1d, - "ref_roots": _1d, - "overlap_matrices": (max_cycles, ovlp_state_num, ovlp_state_num), - "cdd_cubes": _1d, - "cdd_imgs": _1d, - } - return data_model - - -class OverlapCalculator(Calculator): - OVLP_TYPE_VERBOSE = { - "wf": "wavefunction overlap", - "tden": "transition density matrix overlap", - "nto": "natural transition orbital overlap", - # As described in 10.1002/jcc.25800 - "nto_org": "original natural transition orbital overlap", - "top": "transition orbital pair overlap", - } - VALID_KEYS = [ - k for k in OVLP_TYPE_VERBOSE.keys() - ] # lgtm [py/non-iterable-in-for-loop] - VALID_CDDS = (None, "calc", "render") - VALID_XY = ("X", "X+Y", "X-Y") - H5_MAP = { - "mo_coeffs": "mo_coeff_list", - "ci_coeffs": "ci_coeff_list", - "coords": "coords_list", - "all_energies": "all_energies_list", - "roots": "roots_list", - "ref_roots": "reference_roots", - } - - def __init__( - self, - *args, - track=False, - ovlp_type="tden", - double_mol=False, - ovlp_with="previous", - XY="X+Y", - adapt_args=(0.5, 0.3, 0.6), - use_ntos=4, - pr_nto=False, - nto_thresh=0.3, - cdds=None, - orient="", - dump_fn="overlap_data.h5", - h5_dump=False, - ncore=0, - conf_thresh=1e-3, - dyn_roots=0, - mos_ref="cur", - mos_renorm=True, - **kwargs, - ): - super().__init__(*args, **kwargs) - - self.track = track - self.ovlp_type = ovlp_type - assert ( - self.ovlp_type in self.OVLP_TYPE_VERBOSE.keys() - ), f"Valid overlap types are {self.VALID_KEYS}" - self.double_mol = double_mol - assert ovlp_with in ("previous", "first", "adapt") - self.ovlp_with = ovlp_with - assert (self.ovlp_type, self.ovlp_with) != ( - "top", - "adapat", - ), "ovlp_type: top and ovlp_with: adapat are not yet compatible" - self.XY = XY - assert self.XY in self.VALID_XY - self.adapt_args = np.abs(adapt_args, dtype=float) - self.adpt_thresh, self.adpt_min, self.adpt_max = self.adapt_args - self.use_ntos = use_ntos - self.pr_nto = pr_nto - self.nto_thresh = nto_thresh - self.cdds = cdds - # When calculation/rendering of charge density differences (CDDs) is - # requested check fore the required programs (Multiwfn/Jmol). If they're - # not available, we fallback to a more sensible command and print a warning. - msg = ( - "'cdds: {0}' requested, but {1} was not found! " - "Falling back to 'cdds: {2}'!\nConsider defining the {1} " - "command in '.pysisyphusrc'." - ) - - jmol_cmd = get_cmd("jmol") - mwfn_cmd = get_cmd("mwfn") - if (self.cdds == "render") and not jmol_cmd: - logger.debug(msg.format(self.cdds, "Jmol", "calc")) - self.cdds = "calc" - if (self.cdds in ("calc", "render")) and not mwfn_cmd: - logger.debug(msg.format(self.cdds, "Multiwfn", None)) - self.cdds = None - self.log(f"cdds: {self.cdds}, jmol={jmol_cmd}, mwfn={mwfn_cmd}") - assert self.cdds in self.VALID_CDDS - self.orient = orient - self.dump_fn = self.out_dir / dump_fn - self.h5_dump = h5_dump - self.ncore = int(ncore) - self.conf_thresh = float(conf_thresh) - self.dyn_roots = int(dyn_roots) - if self.dyn_roots != 0: - self.dyn_roots = 0 - self.log("dyn_roots = 0 is hardcoded right now") - self.mos_ref = mos_ref - assert self.mos_ref in ("cur", "ref") - self.mos_renorm = bool(mos_renorm) - - assert self.ncore >= 0, "ncore must be a >= 0!" - - self.wfow = None - self.mo_coeff_list = list() - self.ci_coeff_list = list() - self.X_list = list() - self.Y_list = list() - self.nto_list = list() - self.coords_list = list() - # This list will hold the root indices at the beginning of the cycle - # before any overlap calculation. - self.calculated_roots = list() - # This list will hold the (potentially) updated root after an overlap - # calculation and it may differ from the value stored in - # self.calculated_roots. - self.roots_list = list() - # Roots at the reference states that are used for comparison - self.reference_roots = list() - self.cdd_cubes = list() - self.cdd_imgs = list() - self.all_energies_list = list() - # Why did is there already False in the list? Probably related - # to plotting... - self.root_flips = [ - False, - ] - self.first_root = None - self.overlap_matrices = list() - self.row_inds = list() - # The first overlap calculation can be done in cycle 1, and then - # we compare cycle 1 to cycle 0, regardless of the ovlp_with. - self.ref_cycle = 0 - self.ref_cycles = list() - self.atoms = None - self.root = None - - if track: - self.log( - "Tracking excited states with " - f"{self.OVLP_TYPE_VERBOSE[ovlp_type]}s " - f"between the current and the {self.ovlp_with} geometry." - ) - if self.ovlp_with == "adapt": - self.log(f"Adapt args: {self.adapt_args}") - - self.h5_fn = self.out_dir / "ovlp_data.h5" - self.h5_group_name = self.name - # We can't initialize the HDF5 group as we don't know the shape of - # atoms/coords yet. So we wait until after the first calculation. - self.h5_cycles = 50 - - self._data_model = None - self._h5_initialized = False - - def get_h5_group(self): - if not self._h5_initialized: - reset = True - self._h5_initialized = True - else: - reset = False - h5_group = get_h5_group( - self.h5_fn, self.h5_group_name, self.data_model, reset=reset - ) - return h5_group - - @property - def data_model(self): - if self._data_model is None: - max_cycles = self.h5_cycles - exc_state_num, occ_mo_num, virt_mo_num = self.ci_coeff_list[0].shape - self._data_model = get_data_model( - exc_state_num, - occ_mo_num, - virt_mo_num, - self.ovlp_type, - self.atoms, - max_cycles, - ) - return self._data_model - - @property - def roots_number(self): - return self.root + self.dyn_roots - - def get_indices(self, indices=None): - """ - A new root is determined by selecting the overlap matrix row - corresponding to the reference root and checking for the root - with the highest overlap (at the current geometry). - - The overlap matrix is usually formed by a double loop like: - - overlap_matrix = np.empty((ref_states, cur_states)) - for i, ref_state in enumerate(ref_states): - for j, cur_state in enumerate(cur_states): - overlap_matrix[i, j] = make_overlap(ref_state, cur_state) - - So the reference states run along the rows. Thats why the ref_state index - comes first in the 'indices' tuple. - """ - - if indices is None: - # By default we compare a reference cycle with the current (last) - # cycle, so the second index is -1. - ref, cur = self.ref_cycle, -1 - else: - assert len(indices) == 2 - ref, cur = [int(i) for i in indices] - return (ref, cur) - - @staticmethod - def get_mo_norms(mo_coeffs, ao_ovlp): - """MOs are in rows.""" - # einsum-call is extremely slow - # mo_norms = np.einsum("ki,kj,ij->k", mo_coeffs, mo_coeffs, ao_ovlp) - mo_norms = np.diag(mo_coeffs.dot(ao_ovlp).dot(mo_coeffs.T)) - return mo_norms - - @staticmethod - def renorm_mos(mo_coeffs, ao_ovlp): - norms = OverlapCalculator.get_mo_norms(mo_coeffs, ao_ovlp) - sqrts = np.sqrt(norms) - return mo_coeffs / sqrts[:, None] - - def get_ref_mos(self, ref_mo_coeffs, cur_mo_coeffs): - return { - "ref": ref_mo_coeffs, - "cur": cur_mo_coeffs, - }[self.mos_ref] - - def get_orbital_matrices(self, indices=None, ao_ovlp=None): - """Return MO coefficents and AO overlaps for the given indices. - - If not provided, a AO overlap matrix is constructed from one of - the MO coefficient matrices (controlled by self.mos_ref). Also, - if requested one of the two MO coefficient matrices is re-normalized. - """ - - ref, cur = self.get_indices(indices) - ref_mo_coeffs = self.mo_coeff_list[ref].copy() - cur_mo_coeffs = self.mo_coeff_list[cur].copy() - - ao_ovlp_reconstructed = ao_ovlp is None - if ao_ovlp_reconstructed: - sao_mo_coeffs = cur_mo_coeffs if (self.mos_ref == "cur") else ref_mo_coeffs - self.log(f"Reconstructed S_AO from '{self.mos_ref}' MO coefficients.") - ao_ovlp = self.get_sao_from_mo_coeffs(sao_mo_coeffs) - self.log(f"max(abs(S_AO))={np.abs(ao_ovlp).max():.6f}") - - return_mos = [ref_mo_coeffs, cur_mo_coeffs] - # Only renormalize if requested and we reconstructed the AO overlap matrix. - if self.mos_renorm and ao_ovlp_reconstructed: - # If S_AO was reconstructed from "cur" MOs, then "ref" MOs won't be - # normalized anymore and vice versa. - renorm_ind = 0 if (self.mos_ref == "cur") else 1 - to_renorm = return_mos[renorm_ind] - # norms = self.get_mo_norms(to_renorm, ao_ovlp) - return_mos[renorm_ind] = self.renorm_mos(to_renorm, ao_ovlp) - self.log(f"Renormalized '{('ref', 'cur')[renorm_ind]}' MO coefficients.") - elif self.mos_renorm and (not ao_ovlp_reconstructed): - self.log("Skipped MO re-normalization as 'ao_ovlp' was provided.") - - # return *return_mos, ao_ovlp - # The statement above is only valid in python>=3.8 - norms0 = self.get_mo_norms(return_mos[0], ao_ovlp) - norms1 = self.get_mo_norms(return_mos[1], ao_ovlp) - self.log(f"norm(MOs_0): {describe(norms0)}") - self.log(f"norm(MOs_1): {describe(norms1)}") - return return_mos[0], return_mos[1], ao_ovlp - - @staticmethod - def get_sao_from_mo_coeffs(mo_coeffs): - """Recover AO overlaps from given MO coefficients. - - For MOs in the columns of mo_coeffs: - - S_AO = C⁻¹^T C⁻¹ - S_AO C = C⁻¹^T - (S_AO C)^T = C⁻¹ - C^T S_AO^T = C⁻¹ - C^T S_AO C = I - - Here, MOs are expected to be in rows of mo_coeffs, yielding - - C S_AO C^T = I - """ - mo_coeffs_inv = np.linalg.pinv(mo_coeffs, rcond=1e-8) - ao_ovlp = mo_coeffs_inv.dot(mo_coeffs_inv.T) - return ao_ovlp - - def get_sao_from_mo_coeffs_and_dump(self, mo_coeffs): - ao_ovlp = self.get_sao_from_mo_coeffs(mo_coeffs) - ao_ovlp_fn = self.make_fn("ao_ovlp_rec") - np.savetxt(ao_ovlp_fn, ao_ovlp) - return ao_ovlp - - def get_wf_overlaps(self, indices=None, ao_ovlp=None): - old, new = self.get_indices(indices) - old_cycle = (self.mo_coeff_list[old], self.ci_coeff_list[old]) - new_cycle = (self.mo_coeff_list[new], self.ci_coeff_list[new]) - return self.wfow.wf_overlap(old_cycle, new_cycle, ao_ovlp) - - def wf_overlaps(self, mo_coeffs1, ci_coeffs1, mo_coeffs2, ci_coeffs2, ao_ovlp=None): - cycle1 = (mo_coeffs1, ci_coeffs1) - cycle2 = (mo_coeffs2, ci_coeffs2) - overlaps = self.wfow.wf_overlap(cycle1, cycle2, ao_ovlp=ao_ovlp) - return overlaps - - def wf_overlap_with_calculator(self, calc, ao_ovlp=None): - mo_coeffs1 = self.mo_coeff_list[-1] - ci_coeffs1 = self.ci_coeff_list[-1] - mo_coeffs2 = calc.mo_coeff_list[-1] - ci_coeffs2 = calc.ci_coeff_list[-1] - overlaps = self.wf_overlaps( - mo_coeffs1, ci_coeffs1, mo_coeffs2, ci_coeffs2, ao_ovlp=ao_ovlp - ) - return overlaps - - def get_tden_overlaps(self, indices=None, ao_ovlp=None): - mo_coeffs_ref, mo_coeffs_cur, ao_ovlp = self.get_orbital_matrices( - indices, ao_ovlp - ) - - ref, cur = self.get_indices(indices) - ci_coeffs_ref = self.ci_coeff_list[ref] - ci_coeffs_cur = self.ci_coeff_list[cur] - overlaps = tden_overlaps( - mo_coeffs_ref, ci_coeffs_ref, mo_coeffs_cur, ci_coeffs_cur, ao_ovlp - ) - return overlaps - - def tden_overlap_with_calculator(self, calc, ao_ovlp=None): - mo_coeffs1 = self.mo_coeff_list[-1] - ci_coeffs1 = self.ci_coeff_list[-1] - mo_coeffs2 = calc.mo_coeff_list[-1] - ci_coeffs2 = calc.ci_coeff_list[-1] - overlaps = tden_overlaps( - mo_coeffs1, ci_coeffs1, mo_coeffs2, ci_coeffs2, ao_ovlp=ao_ovlp - ) - return overlaps - - def calculate_state_ntos(self, state_ci_coeffs, mos): - normed = state_ci_coeffs / np.linalg.norm(state_ci_coeffs) - # u, s, vh = np.linalg.svd(state_ci_coeffs) - u, s, vh = np.linalg.svd(normed) - lambdas = s ** 2 - self.log("Normalized transition density vector to 1.") - self.log(f"Sum(lambdas)={np.sum(lambdas):.4f}") - lambdas_str = np.array2string(lambdas[:3], precision=4, suppress_small=True) - self.log(f"First three lambdas: {lambdas_str}") - - occ_mo_num = state_ci_coeffs.shape[0] - occ_mos = mos[:occ_mo_num] - vir_mos = mos[occ_mo_num:] - occ_ntos = occ_mos.T.dot(u) - vir_ntos = vir_mos.T.dot(vh) - return occ_ntos, vir_ntos, lambdas - - def get_nto_overlaps(self, indices=None, ao_ovlp=None, org=False): - ref, cur = self.get_indices(indices) - - if ao_ovlp is None: - ao_ovlp = self.get_sao_from_mo_coeffs_and_dump( - self.get_ref_mos(self.mo_coeff_list[ref], self.mo_coeff_list[cur]) - ) - - ntos_1 = self.nto_list[ref] - ntos_2 = self.nto_list[cur] - if org: - overlaps = self.nto_org_overlaps( - ntos_1, ntos_2, ao_ovlp, nto_thresh=self.nto_thresh - ) - else: - overlaps = self.nto_overlaps(ntos_1, ntos_2, ao_ovlp) - return overlaps - - def nto_overlaps(self, ntos_1, ntos_2, ao_ovlp): - return nto_overlaps(ntos_1, ntos_2, ao_ovlp) - - def nto_org_overlaps(self, ntos_1, ntos_2, ao_ovlp, nto_thresh=0.3): - return nto_org_overlaps(ntos_1, ntos_2, ao_ovlp, nto_thresh=nto_thresh) - - def get_top_differences(self, indices=None, ao_ovlp=None): - """Transition orbital pair. differences""" - C_ref, C_cur, ao_ovlp = self.get_orbital_matrices(indices, ao_ovlp) - S_MO = C_ref @ ao_ovlp @ C_cur.T # Currently MOs are given in rows - - ref, cur = self.get_indices(indices) - fact = 1 / 2 ** 0.5 - # Reference step - Xs_ref = fact * self.X_list[ref] - Ys_ref = fact * self.Y_list[ref] - # Current step - Xs_cur = fact * self.X_list[cur] - Ys_cur = fact * self.Y_list[cur] - - def log_norm(mat, title): - norms = np.linalg.norm(mat, axis=(1, 2)) - norms_str = ", ".join([f"{n:.4f}" for n in norms]) - self.log(f"norms({title}) = ({norms_str})") - - log_norm(Xs_ref, "Xs_ref") - log_norm(Ys_ref, "Ys_ref") - log_norm(Xs_cur, "Xs_cur") - log_norm(Ys_cur, "Ys_cur") - - states_A = Xs_ref.shape[0] - states_B = Xs_cur.shape[0] - - rs = list() - for XAi, YAi in zip(Xs_ref, Ys_ref): - for XBj, YBj in zip(Xs_cur, Ys_cur): - r = top_rAB(XAi, YAi, XBj, YBj, S_MO) - rs.append(r) - rs = np.array(rs).reshape(states_A, states_B) - return rs - - def prepare_overlap_data(self, path): - """Implement calculator specific parsing of MO coefficients and CI - coefficients here. Should return a filename pointing to TURBOMOLE - like mos, a MO coefficient array and a CI coefficient array.""" - raise Exception("Implement me!") - - def dump_overlap_data(self): - if self.h5_dump: - h5_group = self.get_h5_group() - - h5_group.attrs["ovlp_type"] = self.ovlp_type - h5_group.attrs["ovlp_with"] = self.ovlp_with - h5_group.attrs["orient"] = self.orient - h5_group.attrs["atoms"] = np.string_(self.atoms) - - for key, shape in self.data_model.items(): - try: - mapped_key = self.H5_MAP[key] - except KeyError: - mapped_key = key - value = getattr(self, mapped_key) - # Skip this value if the underlying list is empty - if not value: - continue - cur_cycle = self.calc_counter - cur_value = value[-1] - # the CDD strings are currently not yet handled properly - if type(cur_value) == PosixPath: - cur_value = str(cur_value) - continue - if len(shape) > 1: - h5_group[key][cur_cycle, : len(cur_value)] = cur_value - else: - h5_group[key][cur_cycle] = cur_value - - data_dict = { - "mo_coeffs": np.array(self.mo_coeff_list, dtype=float), - "ci_coeffs": np.array(self.ci_coeff_list, dtype=float), - "coords": np.array(self.coords_list, dtype=float), - "all_energies": np.array(self.all_energies_list, dtype=float), - "X": np.array(self.X_list, dtype=float), - "Y": np.array(self.Y_list, dtype=float), - } - if self.root: - root_dict = { - "calculated_roots": np.array(self.calculated_roots, dtype=int), - "roots": np.array(self.roots_list, dtype=int), - "root_flips": np.array(self.root_flips, dtype=bool), - "overlap_matrices": np.array(self.overlap_matrices, dtype=float), - "row_inds": np.array(self.row_inds, dtype=int), - "ref_cycles": np.array(self.ref_cycles, dtype=int), - "ref_roots": np.array(self.reference_roots, dtype=int), - } - data_dict.update(root_dict) - - if self.cdd_cubes: - data_dict["cdd_cubes"] = np.array(self.cdd_cubes, dtype="S") - if self.cdd_imgs: - data_dict["cdd_imgs"] = np.array(self.cdd_imgs, dtype="S") - - with h5py.File(self.dump_fn, "w") as handle: - for key, val in data_dict.items(): - if key in ("ci_coeffs", "X", "Y"): - add_kwargs = { - "compression": "gzip", - "compression_opts": 9, - } - else: - add_kwargs = {} - handle.create_dataset(name=key, dtype=val.dtype, data=val, **add_kwargs) - handle.attrs["ovlp_type"] = self.ovlp_type - handle.attrs["ovlp_with"] = self.ovlp_with - handle.attrs["orient"] = self.orient - handle.attrs["atoms"] = np.array(self.atoms, "S1") - - @staticmethod - def from_overlap_data(h5_fn, set_wfow=False): - calc = OverlapCalculator(track=True) - - root_info = False - with h5py.File(h5_fn) as handle: - try: - ovlp_with = handle["ovlp_with"][()].decode() - ovlp_type = handle["ovlp_type"][()].decode() - except KeyError: - ovlp_with = handle.attrs["ovlp_with"] - ovlp_type = handle.attrs["ovlp_type"] - mo_coeffs = handle["mo_coeffs"][:] - ci_coeffs = handle["ci_coeffs"][:] - all_energies = handle["all_energies"][:] - try: - ref_roots = handle["ref_roots"][:] - roots = handle["roots"][:] - calculated_roots = handle["calculated_roots"][:] - root_info = True - except KeyError: - print(f"Couldn't find root information in '{h5_fn}'.") - try: - calc.X_list = handle["X"][:] - calc.Y_list = handle["Y"][:] - except KeyError: - print(f"Couldn't find X and Y vectors in '{h5_fn}'.") - - calc.ovlp_type = ovlp_type - calc.ovlp_with = ovlp_with - calc.mo_coeff_list = list(mo_coeffs) - calc.ci_coeff_list = list(ci_coeffs) - calc.all_energies_list = list(all_energies) - if root_info: - calc.roots_list = list(roots) - calc.calculated_roots = list(calculated_roots) - try: - calc.first_root = ref_roots[0] - calc.root = calc.first_root - except IndexError: - calc.root = roots[0] - - if (ovlp_type == "wf") or set_wfow: - calc.set_wfow(ci_coeffs[0]) - - return calc - - def set_ntos(self, mo_coeffs, ci_coeffs): - roots = ci_coeffs.shape[0] - ntos_for_cycle = list() - for root in range(roots): - sn_ci_coeffs = ci_coeffs[root] - self.log(f"Calculating NTOs for root {root+1}") - occ_ntos, vir_ntos, lambdas = self.calculate_state_ntos( - sn_ci_coeffs, - mo_coeffs, - ) - pr_nto = lambdas.sum() ** 2 / (lambdas ** 2).sum() - if self.pr_nto: - use_ntos = int(np.round(pr_nto)) - self.log(f"PR_NTO={pr_nto:.2f}") - else: - use_ntos = self.use_ntos - self.log(f"Using {use_ntos} NTOS") - ovlp_occ_ntos = occ_ntos.T[:use_ntos] - ovlp_vir_ntos = vir_ntos.T[:use_ntos] - ovlp_lambdas = lambdas[:use_ntos] - ovlp_lambdas = np.concatenate((ovlp_lambdas, ovlp_lambdas)) - ovlp_ntos = np.concatenate((ovlp_occ_ntos, ovlp_vir_ntos), axis=0) - ntos = NTOs(ntos=ovlp_ntos, lambdas=ovlp_lambdas) - ntos_for_cycle.append(ntos) - self.nto_list.append(ntos_for_cycle) - - def set_wfow(self, ci_coeffs): - occ_mo_num, virt_mo_num = ci_coeffs[0].shape - try: - wfow_mem = self.pal * self.mem - except AttributeError: - wfow_mem = 8000 - self.wfow = WFOWrapper( - occ_mo_num, - virt_mo_num, - calc_number=self.calc_number, - wfow_mem=wfow_mem, - ncore=self.ncore, - conf_thresh=self.conf_thresh, - ) - - def store_overlap_data(self, atoms, coords, path=None, overlap_data=None): - if self.atoms is None: - self.atoms = atoms - - if overlap_data is None: - overlap_data = self.prepare_overlap_data(path) - # Currently, still only restricted calculations are supported, so X and Y - # will always be Xa and Ya. - mo_coeffs, Xa, Ya, all_ens = overlap_data - - X = Xa - Y = Ya - assert mo_coeffs.ndim == 2 - assert all([mat.ndim == 3 for mat in (X, Y)]) - - ao_ovlp = self.get_sao_from_mo_coeffs(mo_coeffs) - mo_coeffs = self.renorm_mos(mo_coeffs, ao_ovlp) - - # When unrestricted calculations are finally supported the default - # 'restricted_norm' must be used! - X, Y = norm_ci_coeffs(X, Y, restricted_norm=1.0) - self.X_list.append(X.copy()) - self.Y_list.append(Y.copy()) - - if self.XY == "X": - ci_coeffs = X - elif self.XY == "X+Y": - ci_coeffs = X + Y - elif self.XY == "X-Y": - ci_coeffs = X - Y - else: - raise Exception( - f"Invalid 'XY' value. Allowed values are: '{self.VALID_XY}'!" - ) - - # Don't create the wf-object when we use a different ovlp method. - if (self.ovlp_type == "wf") and (self.wfow is None): - self.set_wfow(ci_coeffs) - - if self.first_root is None: - self.first_root = self.root - self.log(f"Set first root to {self.first_root}.") - # Used for transition density overlaps - self.mo_coeff_list.append(mo_coeffs) - self.ci_coeff_list.append(ci_coeffs) - self.coords_list.append(coords) - self.calculated_roots.append(self.root) - # We can't calculate any overlaps in the first cycle, so we can't - # compute a new root value. So we store the same value as for - # calculated_roots. - if len(self.ci_coeff_list) < 2: - self.roots_list.append(self.root) - self.all_energies_list.append(all_ens) - # Also store NTOs if requested - if self.ovlp_type in ("nto", "nto_org"): - self.set_ntos(mo_coeffs, ci_coeffs) - - def track_root(self, ovlp_type=None): - """Check if a root flip occured occured compared to the previous cycle - by calculating the overlap matrix wrt. a reference cycle.""" - - if ovlp_type is None: - ovlp_type = self.ovlp_type - - # Nothing to compare to if only one calculation was done yet. - # Nonetheless, dump the first cycle to HDF5. - if len(self.ci_coeff_list) < 2: - self.dump_overlap_data() - self.log( - "Skipping overlap calculation in the first cycle " - "as there is nothing to compare to." - ) - return False - - ao_ovlp = None - # We can only run a double molecule calculation if it is - # implemented for the specific calculator. - if self.double_mol and hasattr(self, "run_double_mol_calculation"): - old, new = self.get_indices() - two_coords = self.coords_list[old], self.coords_list[new] - ao_ovlp = self.run_double_mol_calculation(self.atoms, *two_coords) - elif (self.double_mol is False) and (self.ovlp_type == "wf"): - ao_ovlp = self.get_sao_from_mo_coeffs(self.mo_coeff_list[-1]) - self.log("Creating S_AO by myself to avoid its creation in " "WFOverlap.") - - if ovlp_type == "wf": - overlap_mats = self.get_wf_overlaps(ao_ovlp=ao_ovlp) - overlaps = np.abs(overlap_mats[2]) - # overlaps = overlaps**2 - elif ovlp_type == "tden": - overlaps = self.get_tden_overlaps(ao_ovlp=ao_ovlp) - elif ovlp_type == "nto": - overlaps = self.get_nto_overlaps(ao_ovlp=ao_ovlp) - elif ovlp_type == "nto_org": - overlaps = self.get_nto_overlaps(ao_ovlp=ao_ovlp, org=True) - elif ovlp_type == "top": - top_rs = self.get_top_differences(ao_ovlp=ao_ovlp) - overlaps = 1 - top_rs - else: - raise Exception( - "Invalid overlap type key! Use one of " + ", ".join(self.VALID_KEYS) - ) - self.overlap_matrices.append(overlaps) - overlaps = np.abs(overlaps) - - # In the end we are looking for a root flip compared to the - # previous cycle. - # This is done by comparing the excited states at the current cycle - # to some older cycle (the first, the previous, or some cycle - # in between), by determining the highest overlap in a given row - # of the overlap matrix. - ref_root = self.roots_list[self.ref_cycle] - self.reference_roots.append(ref_root) - # Row index in the overlap matrix. Depends on the root of the reference - # cycle and corresponds to the old root. - row_ind = ref_root - 1 - # With WFOverlaps the ground state is also present and the overlap - # matrix has shape (N+1, N+1) instead of (N, N), with N being the - # number of excited states. - if self.ovlp_type == "wf": - row_ind += 1 - self.row_inds.append(row_ind) - self.ref_cycles.append(self.ref_cycle) - self.log( - f"Reference is cycle {self.ref_cycle}, root {ref_root}. " - f"Analyzing row {row_ind} of the overlap matrix." - ) - - ref_root_row = overlaps[row_ind] - new_root = ref_root_row.argmax() - max_overlap = ref_root_row[new_root] - if self.ovlp_type == "wf": - new_root -= 1 - prev_root = self.root - self.log(f"Root at previous cycle is {prev_root}.") - self.root = new_root + 1 - ref_root_row_str = ", ".join( - [f"{i}: {ov:.2%}" for i, ov in enumerate(ref_root_row)] - ) - self.log(f"Overlaps: {ref_root_row_str}") - root_flip = self.root != prev_root - self.log(f"Highest overlap is {max_overlap:.2%}.") - if not root_flip: - self.log(f"Keeping current root {self.root}.") - else: - self.log( - f"Root flip! New root is {self.root}. Root at previous " - f"step was {prev_root}." - ) - # Look for a new reference state if requested. We want to avoid - # overlap matrices just after a root flip. - if self.ovlp_with == "previous": - self.ref_cycle += 1 - elif (self.ovlp_with == "adapt") and not root_flip: - self.log("Checking wether the reference cycle has to be adapted.") - sorted_inds = ref_root_row.argsort() - sec_highest, highest = ref_root_row[sorted_inds[-2:]] - ratio = sec_highest / highest - self.log( - f"Two highest overlaps: {sec_highest:.2%}, {highest:.2%}, " - f"ratio={ratio:.4f}" - ) - above_thresh = highest >= self.adpt_thresh - self.log( - f"Highest overlap is above threshold? (>= {self.adpt_thresh:.4f}): " - f"{above_thresh}" - ) - valid_ratio = self.adpt_min < ratio < self.adpt_max - self.log( - f"Ratio is valid? (between {self.adpt_min:.4f} and " - f"{self.adpt_max:.4f}): {valid_ratio}" - ) - """Only adapt the reference cycle when the overlaps are well - behaved and the following two conditions are True: - - 1.) The highest overlap is above the threshold. - - 2.) The ratio value of (second highest)/(highest) is valid. A small - value indicates cleary separated states and we probably - don't have to update the reference cycle as the overlaps are still - big enough. - As the overlaps between two states become more similar the ratio - approaches 1. This may occur in regions of state crossings and then - we dont't want to update the reference cycle. - """ - if above_thresh and valid_ratio: - self.ref_cycle = len(self.calculated_roots) - 1 - - if self.ref_cycle != self.ref_cycles[-1]: - self.log(f"New reference cycle is {self.ref_cycle}.") - else: - self.log(f"Keeping old reference cycle {self.ref_cycle}.") - - self.root_flips.append(root_flip) - self.roots_list.append(self.root) - assert len(self.roots_list) == len(self.calculated_roots) - - if self.cdds: - try: - self.calc_cdd_cube(self.root) - except Exception as err: - print("CDD calculation by Multiwfn crashed. Disabling it!") - self.log(err) - self.cdds = None - if self.cdds == "render": - self.render_cdd_cube() - - self.dump_overlap_data() - self.log("\n") - - # True if a root flip occured - return root_flip - - def calc_cdd_cube(self, root, cycle=-1): - # Check if Calculator provides an input file (.fchk/.molden) for Mwfn - if not hasattr(self, "mwfn_wf"): - self.log( - "Calculator does not provide an input file for Multiwfn, " - "as 'self.mwfn_wf' is not set! Skipping CDD cube generation!" - ) - if cycle != -1: - self.log("'cycle' argument to make_cdd_cube is currently ignored!") - energies = self.all_energies_list[cycle] - ci_coeffs = self.ci_coeff_list[cycle] - exc_str = get_mwfn_exc_str(energies, ci_coeffs) - with tempfile.TemporaryDirectory() as tmp_dir: - tmp_path = Path(tmp_dir) - exc_path = tmp_path / "exc_input" - with open(exc_path, "w") as handle: - handle.write(exc_str) - cubes = make_cdd(self.mwfn_wf, root, str(exc_path), tmp_path) - assert len(cubes) == 1 - cube = cubes[0] - cube_fn = cubes[0].name - new_cube_fn = self.make_fn(cube_fn) - shutil.copy(cube, new_cube_fn) - self.cdd_cubes.append(new_cube_fn) - - def render_cdd_cube(self): - cdd_cube = self.cdd_cubes[-1] - try: - cdd_img = render_cdd_cube_jmol(cdd_cube, orient=self.orient) - self.cdd_imgs.append(cdd_img) - except: - self.log("Something went wrong while rendering the CDD cube.") diff --git a/deprecated/cos/GrowingNT.py b/deprecated/cos/GrowingNT.py deleted file mode 100644 index 5dfe90299c..0000000000 --- a/deprecated/cos/GrowingNT.py +++ /dev/null @@ -1,79 +0,0 @@ -#/!usr/bin/env python3 - -# See [1] 10.1063/1.1885467 - -from copy import copy - -import numpy as np - -from pysisyphus.cos.ChainOfStates import ChainOfStates -from pysisyphus.Geometry import Geometry - - -class GrowingNT(ChainOfStates): - - def __init__(self, images, calc_getter, eps, damp, max_nodes=10, - readjust=True, **kwargs): - super().__init__(images, **kwargs) - - self.calc_getter = calc_getter - self.max_nodes = max_nodes - self.eps = eps - self.damp = damp - self.readjust = readjust - - self.I = np.eye(self.images[0].coords.size) - - def new_node_coords(self, k): - l = (self.max_nodes-k) / (self.max_nodes+1-k) - kth_coords = self.images[k].coords - last_coords = self.images[-1].coords - new_coords = l*kth_coords + (1-l)*last_coords - return new_coords - - def set_new_node(self, k): - new_coords = self.new_node_coords(k) - new_node = Geometry(self.image_atoms, new_coords) - new_node.set_calculator(self.calc_getter()) - self.images.insert(k+1, new_node) - # print(f"made node {k+1}") - return new_node - - def run(self): - # add_ks = np.arange(self.max_nodes-len(self.images)) - add_ks = np.arange(self.max_nodes) - # import pdb; pdb.set_trace() - # Initial rStart at - # grad0 = self.images[0].gradient - # norm_grad0 = grad0 / np.linalg.norm(grad0) - r = self.get_tangent(0)[:,None] - # r = norm_grad0[:,None] - - self.points = [self.images[0].coords] - self.conv_points = [self.images[0].coords] - readjusted = False - for k in add_ks: - new_node = self.set_new_node(k) - if self.readjust and (not readjusted) and k > self.max_nodes / 2: - # Adapt search direction - print("old search direction", r) - r = self.get_tangent(k+1)[:,None] - print("adapted search direction", r) - readjusted = True - # print("new node", new_node.coords) - Dr = r.dot(r.T) - Pr = self.I - Dr - # Do projections and correction - for i in range(45): - grad = new_node.gradient - proj_grad = Pr.dot(grad) - norm = np.linalg.norm(proj_grad) - # print(f"cycle {i} norm is {norm:.3f}") - if norm < self.eps: - print(f"{i:02d} microcycles, norm={norm:.4f}") - break - p = -self.damp*proj_grad - new_node.coords += p - self.points.append(new_node.coords.copy()) - self.conv_points.append(new_node.coords.copy()) - self.conv_points.append(self.images[-1].coords.copy()) diff --git a/deprecated/intcoords/InternalCoordinatesOld.py b/deprecated/intcoords/InternalCoordinatesOld.py deleted file mode 100644 index de55bec2ea..0000000000 --- a/deprecated/intcoords/InternalCoordinatesOld.py +++ /dev/null @@ -1,714 +0,0 @@ -#!/usr/bin/env python3 - -# [1] https://doi.org/10.1063/1.1515483 optimization review -# [2] https://doi.org/10.1063/1.471864 delocalized internal coordinates -# [3] https://doi.org/10.1016/0009-2614(95)00646-L lindh model hessian -# [4] 10.1002/(SICI)1096-987X(19990730)20:10<1067::AID-JCC9>3.0.CO;2-V -# Handling of corner cases -# [5] https://doi.org/10.1063/1.462844 - -from collections import namedtuple -from functools import reduce -import itertools as it -import logging - -import numpy as np -from scipy.spatial.distance import pdist, squareform - -from pysisyphus.constants import BOHR2ANG -from pysisyphus.elem_data import VDW_RADII, COVALENT_RADII as CR -from pysisyphus.intcoords.derivatives import d2q_b, d2q_a, d2q_d -from pysisyphus.intcoords.exceptions import NeedNewInternalsException -from pysisyphus.intcoords.findbonds import get_pair_covalent_radii -from pysisyphus.intcoords.fragments import merge_fragments - - -PrimitiveCoord = namedtuple( - "PrimitiveCoord", - "inds val grad", -) - - -class RedundantCoords: - - RAD_175 = 3.05432619 - BEND_MIN_DEG = 15 - BEND_MAX_DEG = 180 - - def __init__(self, atoms, cart_coords, bond_factor=1.3, - prim_indices=None, define_prims=None, bonds_only=False, - check_bends=True, check_dihedrals=False): - self.atoms = atoms - self._cart_coords = cart_coords - self.bond_factor = bond_factor - self.define_prims = define_prims - self.bonds_only = bonds_only - self.check_bends = check_bends - self.check_dihedrals = check_dihedrals - - self._B_prim = None - self.bond_indices = list() - self.bending_indices = list() - self.dihedral_indices = list() - self.hydrogen_bond_indices = list() - - if prim_indices is None: - self.set_primitive_indices(self.define_prims) - else: - to_arr = lambda _: np.array(list(_), dtype=int) - bonds, bends, dihedrals = prim_indices - # We accept all bond indices. What could possibly go wrong?! :) - self.bond_indices = to_arr(bonds) - valid_bends = [inds for inds in bends - if self.is_valid_bend(inds)] - self.bending_indices = to_arr(valid_bends) - valid_dihedrals = [inds for inds in dihedrals if - self.is_valid_dihedral(inds)] - self.dihedral_indices = to_arr(valid_dihedrals) - - if self.bonds_only: - self.bending_indices = list() - self.dihedral_indices = list() - self._prim_internals = self.calculate(self.cart_coords) - self._prim_coords = np.array([pc.val for pc in self._prim_internals]) - - def log(self, message): - logger = logging.getLogger("internal_coords") - logger.debug(message) - - @property - def prim_indices(self): - return [self.bond_indices, self.bending_indices, self.dihedral_indices] - - @property - def prim_indices_set(self): - return set([tuple(prim_ind) for prim_ind in it.chain(*self.prim_indices)]) - - @property - def prim_coords(self): - if self._prim_coords is None: - self._prim_coords = np.array( - [pc.val for pc in self.calculate(self.cart_coords)] - ) - return self._prim_coords - - @property - def cart_coords(self): - return self._cart_coords - - @cart_coords.setter - def cart_coords(self, cart_coords): - self._cart_coords = cart_coords - self._B_prim = None - - @property - def coords(self): - return self.prim_coords - - @property - def coord_indices(self): - ic_ind_tuples = [tuple(ic.inds) for ic in self._prim_internals] - return {ic_inds: i for i, ic_inds in enumerate(ic_ind_tuples)} - - @property - def dihed_start(self): - return len(self.bond_indices) + len(self.bending_indices) - - def get_index_of_prim_coord(self, prim_ind): - """Index of primitive internal for the given atom indices. - - TODO: simplify this so when we get a prim_ind of len 2 - (bond) we don't have to check the bending and dihedral indices.""" - prim_ind_set = set(prim_ind) - indices = [i for i, pi in enumerate(it.chain(*self.prim_indices)) - if set(pi) == prim_ind_set] - index = None - try: - index = indices[0] - except IndexError: - self.log(f"Primitive internal with indices {prim_ind} " - "is not defined!") - return index - - @property - def c3d(self): - return self.cart_coords.reshape(-1, 3) - - @property - def B_prim(self): - """Wilson B-Matrix""" - if self._B_prim is None: - self._B_prim = np.array([c.grad for c in self.calculate(self.cart_coords)]) - - return self._B_prim - - @property - def B(self): - """Wilson B-Matrix""" - return self.B_prim - - @property - def Bt_inv(self): - """Transposed generalized inverse of the Wilson B-Matrix.""" - B = self.B - return np.linalg.pinv(B.dot(B.T)).dot(B) - - @property - def B_inv(self): - """Generalized inverse of the Wilson B-Matrix.""" - B = self.B - return B.T.dot(np.linalg.pinv(B.dot(B.T))) - - @property - def P(self): - """Projection matrix onto B. See [1] Eq. (4).""" - return self.B.dot(self.B_inv) - - def transform_forces(self, cart_forces): - """Combination of Eq. (9) and (11) in [1].""" - return self.Bt_inv.dot(cart_forces) - - def get_K_matrix(self, int_gradient=None): - if int_gradient is not None: - assert len(int_gradient) == len(self._prim_internals) - size_ = self.cart_coords.size - if int_gradient is None: - return np.zeros((size_, size_)) - - dg_funcs = { - 2: d2q_b, - 3: d2q_a, - 4: d2q_d, - } - def grad_deriv_wrapper(inds): - coords_flat = self.c3d[inds].flatten() - dgrad = dg_funcs[len(inds)](*coords_flat) - return dgrad - - K_flat = np.zeros(size_ * size_) - for pc, int_grad_item in zip(self._prim_internals, int_gradient): - # Contract with gradient - try: - dg = int_grad_item * grad_deriv_wrapper(pc.inds) - except (ValueError, ZeroDivisionError) as err: - self.log( "Error in calculation of 2nd derivative of primitive " - f"internal {pc.inds}." - ) - continue - # Depending on the type of internal coordinate dg is a flat array - # of size 36 (stretch), 81 (bend) or 144 (torsion). - # - # An internal coordinate contributes to an element K[j, k] of the - # K matrix if the cartesian coordinate indices j and k belong to an - # atom that contributes to the respective internal coordinate. - # - # As for now we build up the K matrix as flat array. To add the dg - # entries at the appropriate places in K_flat we have to calculate - # the corresponding flat indices of dg in K_flat. - cart_inds = list(it.chain(*[range(3*i,3*i+3) for i in pc.inds])) - flat_inds = [row*size_ + col for row, col in it.product(cart_inds, cart_inds)] - K_flat[flat_inds] += dg - K = K_flat.reshape(size_, size_) - return K - - def transform_hessian(self, cart_hessian, int_gradient=None): - """Transform Cartesian Hessian to internal coordinates.""" - if int_gradient is None: - self.log("Supplied 'int_gradient' is None. K matrix will be zero, " - "so derivatives of the Wilson-B-matrix are neglected in " - "the hessian transformation." - ) - K = self.get_K_matrix(int_gradient) - return self.Bt_inv.dot(cart_hessian-K).dot(self.B_inv) - - def backtransform_hessian(self, redund_hessian, int_gradient=None): - """Transform Hessian in internal coordinates to Cartesians.""" - if int_gradient is None: - self.log("Supplied 'int_gradient' is None. K matrix will be zero, " - "so derivatives of the Wilson-B-matrix are neglected in " - "the hessian transformation." - ) - K = self.get_K_matrix(int_gradient) - return self.B.T.dot(redund_hessian).dot(self.B) + K - - def project_hessian(self, H, shift=1000): - """Expects a hessian in internal coordinates. See Eq. (11) in [1].""" - P = self.P - return P.dot(H).dot(P) + shift*(np.eye(P.shape[0]) - P) - - def project_vector(self, vector): - """Project supplied vector onto range of B.""" - return self.P.dot(vector) - - def connect_fragments(self, cdm, fragments): - """Determine the smallest interfragment bond for a list - of fragments and a condensed distance matrix.""" - dist_mat = squareform(cdm) - interfragment_indices = list() - for frag1, frag2 in it.combinations(fragments, 2): - arr1 = np.array(list(frag1))[None,:] - arr2 = np.array(list(frag2))[:,None] - indices = [(i1, i2) for i1, i2 in it.product(frag1, frag2)] - distances = np.array([dist_mat[ind] for ind in indices]) - min_index = indices[distances.argmin()] - interfragment_indices.append(min_index) - # Or as Philipp proposed: two loops over the fragments and only - # generate interfragment distances. So we get a full matrix with - # the original indices but only the required distances. - return interfragment_indices - - def set_hydrogen_bond_indices(self, bond_indices): - coords3d = self.cart_coords.reshape(-1, 3) - tmp_sets = [frozenset(bi) for bi in bond_indices] - # Check for hydrogen bonds as described in [1] A.1 . - # Find hydrogens bonded to small electronegative atoms X = (N, O - # F, P, S, Cl). - hydrogen_inds = [i for i, a in enumerate(self.atoms) - if a.lower() == "h"] - x_inds = [i for i, a in enumerate(self.atoms) - if a.lower() in "n o f p s cl".split()] - hydrogen_bond_inds = list() - for h_ind, x_ind in it.product(hydrogen_inds, x_inds): - as_set = set((h_ind, x_ind)) - if not as_set in tmp_sets: - continue - # Check if distance of H to another electronegative atom Y is - # greater than the sum of their covalent radii but smaller than - # the 0.9 times the sum of their van der Waals radii. If the - # angle X-H-Y is greater than 90° a hydrogen bond is asigned. - y_inds = set(x_inds) - set((x_ind, )) - for y_ind in y_inds: - y_atom = self.atoms[y_ind].lower() - cov_rad_sum = CR["h"] + CR[y_atom] - distance = self.calc_stretch(coords3d, (h_ind, y_ind)) - vdw = 0.9 * (VDW_RADII["h"] + VDW_RADII[y_atom]) - angle = self.calc_bend(coords3d, (x_ind, h_ind, y_ind)) - if (cov_rad_sum < distance < vdw) and (angle > np.pi/2): - self.hydrogen_bond_indices.append((h_ind, y_ind)) - self.log(f"Added hydrogen bond between atoms {h_ind} " - f"({self.atoms[h_ind]}) and {y_ind} ({self.atoms[y_ind]})") - self.hydrogen_bond_indices = np.array(self.hydrogen_bond_indices) - - def set_bond_indices(self, define_bonds=None, factor=None): - """ - Default factor of 1.3 taken from [1] A.1. - Gaussian uses somewhat less, like 1.2, or different radii than we do. - """ - bond_factor = factor if factor else self.bond_factor - coords3d = self.cart_coords.reshape(-1, 3) - # Condensed distance matrix - cdm = pdist(coords3d) - # Generate indices corresponding to the atom pairs in the - # condensed distance matrix cdm. - atom_indices = list(it.combinations(range(len(coords3d)),2)) - atom_indices = np.array(atom_indices, dtype=int) - cov_rad_sums = get_pair_covalent_radii(self.atoms) - cov_rad_sums *= bond_factor - bond_flags = cdm <= cov_rad_sums - bond_indices = atom_indices[bond_flags] - - if define_bonds: - bond_indices = np.concatenate(((bond_indices, define_bonds)), axis=0) - - self.bare_bond_indices = bond_indices - - # Look for hydrogen bonds - self.set_hydrogen_bond_indices(bond_indices) - if self.hydrogen_bond_indices.size > 0: - bond_indices = np.concatenate((bond_indices, - self.hydrogen_bond_indices)) - - # Merge bond index sets into fragments - bond_ind_sets = [frozenset(bi) for bi in bond_indices] - fragments = merge_fragments(bond_ind_sets) - - # Look for unbonded single atoms and create fragments for them. - bonded_set = set(tuple(bond_indices.flatten())) - unbonded_set = set(range(len(self.atoms))) - bonded_set - fragments.extend( - [frozenset((atom, )) for atom in unbonded_set] - ) - self.fragments = fragments - - # Check if there are any disconnected fragments. If there are some - # create interfragment bonds between all of them. - if len(fragments) != 1: - interfragment_inds = self.connect_fragments(cdm, fragments) - bond_indices = np.concatenate((bond_indices, interfragment_inds)) - - self.bond_indices = bond_indices - - def are_parallel(self, vec1, vec2, angle_ind=None, thresh=1e-6): - dot = max(min(vec1.dot(vec2), 1), -1) - rad = np.arccos(dot)#vec1.dot(vec2)) - # angle > 175° - if abs(rad) > self.RAD_175: - # self.log(f"Nearly linear angle {angle_ind}: {np.rad2deg(rad)}") - ind_str = f" ({angle_ind})" if (angle_ind is not None) else "" - self.log(f"Nearly linear angle{ind_str}: {np.rad2deg(rad)}") - return abs(rad) > (np.pi - thresh) - - def sort_by_central(self, set1, set2): - """Determines a common index in two sets and returns a length 3 - tuple with the central index at the middle position and the two - terminal indices as first and last indices.""" - central_set = set1 & set2 - union = set1 | set2 - assert len(central_set) == 1 - terminal1, terminal2 = union - central_set - (central, ) = central_set - return (terminal1, central, terminal2), central - - def is_valid_bend(self, bend_ind): - val = self.calc_bend(self.c3d, bend_ind) - deg = np.rad2deg(val) - # Always return true if bends should not be checked - return ( - not self.check_bends) or (self.BEND_MIN_DEG <= deg <= self.BEND_MAX_DEG - ) - - def set_bending_indices(self, define_bends=None): - bond_sets = {frozenset(bi) for bi in self.bond_indices} - for bond_set1, bond_set2 in it.combinations(bond_sets, 2): - union = bond_set1 | bond_set2 - if len(union) == 3: - as_tpl, _ = self.sort_by_central(bond_set1, bond_set2) - if not self.is_valid_bend(as_tpl): - self.log(f"Didn't create bend {list(as_tpl)}") - # f" with value of {deg:.3f}°") - continue - self.bending_indices.append(as_tpl) - self.bending_indices = np.array(self.bending_indices, dtype=int) - - if define_bends: - bis = np.concatenate(( (self.bending_indices, define_bends)), axis=0) - self.bending_indices = bis - - def is_valid_dihedral(self, dihedral_ind, thresh=1e-6): - # Check for linear atoms - first_angle = self.calc_bend(self.c3d, dihedral_ind[:3]) - second_angle = self.calc_bend(self.c3d, dihedral_ind[1:]) - pi_thresh = np.pi - thresh - return ((abs(first_angle) < pi_thresh) - and (abs(second_angle) < pi_thresh) - ) - - def set_dihedral_indices(self, define_dihedrals=None): - dihedrals = list() - def set_dihedral_index(dihedral_ind): - dihed = tuple(dihedral_ind) - # Check if this dihedral is already present - if (dihed in dihedrals) or (dihed[::-1] in dihedrals): - return - # Assure that the angles are below 175° (3.054326 rad) - if not self.is_valid_dihedral(dihedral_ind, thresh=0.0873): - self.log(f"Skipping generation of dihedral {dihedral_ind} " - "as some of the the atoms are (nearly) linear." - ) - return - self.dihedral_indices.append(dihedral_ind) - dihedrals.append(dihed) - - improper_dihedrals = list() - coords3d = self.cart_coords.reshape(-1, 3) - for bond, bend in it.product(self.bond_indices, self.bending_indices): - central = bend[1] - bend_set = set(bend) - bond_set = set(bond) - # Check if the two sets share one common atom. If not continue. - intersect = bend_set & bond_set - if len(intersect) != 1: - continue - # When the common atom is a terminal atom of the bend, that is - # it's not the central atom of the bend, we create a - # proper dihedral. Before we create any improper dihedrals we - # create these proper dihedrals. - if central not in bond_set: - # The new terminal atom in the dihedral is the one that - # doesn' intersect. - terminal = tuple(bond_set - intersect)[0] - intersecting_atom = tuple(intersect)[0] - if intersecting_atom == bend[0]: - dihedral_ind = [terminal] + bend.tolist() - else: - dihedral_ind = bend.tolist() + [terminal] - set_dihedral_index(dihedral_ind) - # If the common atom is the central atom we try to form an out - # of plane bend / improper torsion. They may be created later on. - else: - fourth_atom = list(bond_set - intersect) - dihedral_ind = bend.tolist() + fourth_atom - # This way dihedrals may be generated that contain linear - # atoms and these would be undefinied. So we check for this. - dihed = self.calc_dihedral(coords3d, dihedral_ind) - if not np.isnan(dihed): - improper_dihedrals.append(dihedral_ind) - else: - self.log(f"Dihedral {dihedral_ind} is undefinied. Skipping it!") - - # Now try to create the remaining improper dihedrals. - if (len(self.atoms) >= 4) and (len(self.dihedral_indices) == 0): - for improp in improper_dihedrals: - set_dihedral_index(improp) - self.log("Permutational symmetry not considerd in " - "generation of improper dihedrals.") - - self.dihedral_indices = np.array(self.dihedral_indices) - - if define_dihedrals: - dis = np.concatenate(((self.dihedral_indices, define_dihedrals)), axis=0) - self.dihedral_indices = dis - - def sort_by_prim_type(self, to_sort): - by_prim_type = [[], [], []] - if to_sort is None: - to_sort = list() - for item in to_sort: - len_ = len(item) - by_prim_type[len_-2].append(item) - return by_prim_type - - def set_primitive_indices(self, define_prims=None): - stretches, bends, dihedrals = self.sort_by_prim_type(define_prims) - self.set_bond_indices(stretches) - self.set_bending_indices(bends) - self.set_dihedral_indices(dihedrals) - - def calculate(self, coords, attr=None): - coords3d = coords.reshape(-1, 3) - def per_type(func, ind): - val, grad = func(coords3d, ind, True) - return PrimitiveCoord(ind, val, grad) - self.bonds = list() - self.bends = list() - self.dihedrals = list() - for ind in self.bond_indices: - bonds = per_type(self.calc_stretch, ind) - self.bonds.append(bonds) - for ind in self.bending_indices: - bend = per_type(self.calc_bend, ind) - self.bends.append(bend) - for ind in self.dihedral_indices: - dihedral = per_type(self.calc_dihedral, ind) - self.dihedrals.append(dihedral) - int_coords = self.bonds + self.bends + self.dihedrals - if attr: - return np.array([getattr(ic,attr) for ic in int_coords]) - return int_coords - - def calc_stretch(self, coords3d, bond_ind, grad=False): - n, m = bond_ind - bond = coords3d[m] - coords3d[n] - bond_length = np.linalg.norm(bond) - if grad: - bond_normed = bond / bond_length - row = np.zeros_like(coords3d) - # 1 / -1 correspond to the sign factor [1] Eq. 18 - row[m,:] = bond_normed - row[n,:] = -bond_normed - row = row.flatten() - return bond_length, row - return bond_length - - def calc_bend(self, coords3d, angle_ind, grad=False): - m, o, n = angle_ind - u_dash = coords3d[m] - coords3d[o] - v_dash = coords3d[n] - coords3d[o] - u_norm = np.linalg.norm(u_dash) - v_norm = np.linalg.norm(v_dash) - u = u_dash / u_norm - v = v_dash / v_norm - angle_rad = np.arccos(u.dot(v)) - if grad: - # Eq. (24) in [1] - if self.are_parallel(u, v, angle_ind): - tmp_vec = np.array((1, -1, 1)) - par = self.are_parallel(u, tmp_vec) and self.are_parallel(v, tmp_vec) - tmp_vec = np.array((-1, 1, 1)) if par else tmp_vec - w_dash = np.cross(u, tmp_vec) - else: - w_dash = np.cross(u, v) - w_norm = np.linalg.norm(w_dash) - w = w_dash / w_norm - uxw = np.cross(u, w) - wxv = np.cross(w, v) - - row = np.zeros_like(coords3d) - # | m | n | o | - # ----------------------------------- - # sign_factor(amo) | 1 | 0 | -1 | first_term - # sign_factor(ano) | 0 | 1 | -1 | second_term - first_term = uxw / u_norm - second_term = wxv / v_norm - row[m,:] = first_term - row[o,:] = -first_term - second_term - row[n,:] = second_term - row = row.flatten() - return angle_rad, row - return angle_rad - - def calc_dihedral(self, coords3d, dihedral_ind, grad=False, cos_tol=1e-9): - m, o, p, n = dihedral_ind - u_dash = coords3d[m] - coords3d[o] - v_dash = coords3d[n] - coords3d[p] - w_dash = coords3d[p] - coords3d[o] - u_norm = np.linalg.norm(u_dash) - v_norm = np.linalg.norm(v_dash) - w_norm = np.linalg.norm(w_dash) - u = u_dash / u_norm - v = v_dash / v_norm - w = w_dash / w_norm - phi_u = np.arccos(u.dot(w)) - phi_v = np.arccos(-w.dot(v)) - uxw = np.cross(u, w) - vxw = np.cross(v, w) - cos_dihed = uxw.dot(vxw)/(np.sin(phi_u)*np.sin(phi_v)) - - # Restrict cos_dihed to [-1, 1] - if cos_dihed >= 1 - cos_tol: - dihedral_rad = 0 - elif cos_dihed <= -1 + cos_tol: - dihedral_rad = np.arccos(-1) - else: - dihedral_rad = np.arccos(cos_dihed) - - if dihedral_rad != np.pi: - # wxv = np.cross(w, v) - # if wxv.dot(u) < 0: - if vxw.dot(u) < 0: - dihedral_rad *= -1 - if grad: - row = np.zeros_like(coords3d) - # | m | n | o | p | - # ------------------------------------------ - # sign_factor(amo) | 1 | 0 | -1 | 0 | 1st term - # sign_factor(apn) | 0 | -1 | 0 | 1 | 2nd term - # sign_factor(aop) | 0 | 0 | 1 | -1 | 3rd term - # sign_factor(apo) | 0 | 0 | -1 | 1 | 4th term - sin2_u = np.sin(phi_u)**2 - sin2_v = np.sin(phi_v)**2 - first_term = uxw/(u_norm*sin2_u) - second_term = vxw/(v_norm*sin2_v) - third_term = uxw*np.cos(phi_u)/(w_norm*sin2_u) - fourth_term = -vxw*np.cos(phi_v)/(w_norm*sin2_v) - row[m,:] = first_term - row[n,:] = -second_term - row[o,:] = -first_term + third_term - fourth_term - row[p,:] = second_term - third_term + fourth_term - row = row.flatten() - return dihedral_rad, row - return dihedral_rad - - def update_internals(self, new_cartesians, prev_internals): - new_internals = self.calculate(new_cartesians, attr="val") - internal_diffs = np.array(new_internals - prev_internals) - _, _, dihedrals = self.prim_indices - dihedral_diffs = internal_diffs[-len(dihedrals):] - # Find differences that are shifted by 2*pi - shifted_by_2pi = np.abs(np.abs(dihedral_diffs) - 2*np.pi) < np.pi/2 - org = dihedral_diffs.copy() - new_dihedrals = new_internals[-len(dihedrals):] - new_dihedrals[shifted_by_2pi] -= 2*np.pi * np.sign(dihedral_diffs[shifted_by_2pi]) - new_internals[-len(dihedrals):] = new_dihedrals - return new_internals - - def dihedrals_are_valid(self, cart_coords): - _, _, dihedrals = self.prim_indices - - def collinear(v1, v2, thresh=1e-4): - # ~4e-5 corresponds to 179.5° - return 1 - abs(v1.dot(v2)) <= thresh - - coords3d = cart_coords.reshape(-1, 3) - def check(indices): - m, o, p, n = indices - u_dash = coords3d[m] - coords3d[o] - v_dash = coords3d[n] - coords3d[p] - w_dash = coords3d[p] - coords3d[o] - u_norm = np.linalg.norm(u_dash) - v_norm = np.linalg.norm(v_dash) - w_norm = np.linalg.norm(w_dash) - u = u_dash / u_norm - v = v_dash / v_norm - w = w_dash / w_norm - - valid = not (collinear(u, w) or collinear(v, w)) - return valid - - all_valid = all([check(indices) for indices in dihedrals]) - return all_valid - - def transform_int_step(self, step, cart_rms_thresh=1e-6): - """This is always done in primitive internal coordinates so care - has to be taken that the supplied step is given in primitive internal - coordinates.""" - - remaining_int_step = step - cur_cart_coords = self.cart_coords.copy() - cur_internals = self.prim_coords - target_internals = cur_internals + step - B_prim = self.B_prim - - # Bt_inv may be overriden in other coordiante systems so we - # calculate it 'manually' here. - Bt_inv_prim = np.linalg.pinv(B_prim.dot(B_prim.T)).dot(B_prim) - - last_rms = 9999 - prev_internals = cur_internals - self.backtransform_failed = True - for i in range(25): - cart_step = Bt_inv_prim.T.dot(remaining_int_step) - # Recalculate exact Bt_inv every cycle. Costly. - # cart_step = self.Bt_inv.T.dot(remaining_int_step) - cart_rms = np.sqrt(np.mean(cart_step**2)) - # Update cartesian coordinates - cur_cart_coords += cart_step - # Determine new internal coordinates - new_internals = self.update_internals(cur_cart_coords, prev_internals) - remaining_int_step = target_internals - new_internals - internal_rms = np.sqrt(np.mean(remaining_int_step**2)) - self.log(f"Cycle {i}: rms(Δcart)={cart_rms:1.4e}, " - f"rms(Δinternal) = {internal_rms:1.5e}" - ) - - # This assumes the first cart_rms won't be > 9999 ;) - if (cart_rms < last_rms): - # Store results of the conversion cycle for laster use, if - # the internal-cartesian-transformation goes bad. - best_cycle = (cur_cart_coords.copy(), new_internals.copy()) - best_cycle_ind = i - elif i != 0: - # If the conversion somehow fails we return the step - # saved above. - self.log( "Internal to cartesian failed! Using from step " - f"from cycle {best_cycle_ind}." - ) - cur_cart_coords, new_internals = best_cycle - break - else: - raise Exception("Internal-cartesian back-transformation already " - "failed in the first step. Aborting!" - ) - prev_internals = new_internals - - last_rms = cart_rms - if cart_rms < cart_rms_thresh: - self.log("Internal to cartesian transformation converged!") - self.backtransform_failed = False - break - self._prim_coords = np.array(new_internals) - - if self.check_dihedrals and (not self.dihedrals_are_valid(cur_cart_coords)): - raise NeedNewInternalsException(cur_cart_coords) - - self.log("") - # Return the difference between the new cartesian coordinates that yield - # the desired internal coordinates and the old cartesian coordinates. - return cur_cart_coords - self.cart_coords - - def __str__(self): - bonds = len(self.bond_indices) - bends = len(self.bending_indices) - dihedrals = len(self.dihedral_indices) - name = self.__class__.__name__ - return f"{name}({bonds} bonds, {bends} bends, {dihedrals} dihedrals)" diff --git a/deprecated/intcoords/autodiff.py b/deprecated/intcoords/autodiff.py deleted file mode 100644 index 8631a63faa..0000000000 --- a/deprecated/intcoords/autodiff.py +++ /dev/null @@ -1,64 +0,0 @@ -#!/usr/bin/env python3 - -import autograd.numpy as np -from autograd import grad - - -def calc_stretch(coords3d, bond_ind): - n, m = bond_ind - bond = coords3d[m] - coords3d[n] - bond_length = np.linalg.norm(bond) - return bond_length - -stretch_grad = grad(calc_stretch) - - -def calc_bend(coords3d, angle_ind): - m, o, n = angle_ind - u_dash = coords3d[m] - coords3d[o] - v_dash = coords3d[n] - coords3d[o] - u_norm = np.linalg.norm(u_dash) - v_norm = np.linalg.norm(v_dash) - u = u_dash / u_norm - v = v_dash / v_norm - angle_rad = np.arccos(np.dot(u, v)) - return angle_rad - - -bend_grad = grad(calc_bend) - - -def calc_dihedral(coords3d, dihedral_ind, cos_tol=1e-9): - m, o, p, n = dihedral_ind - u_dash = coords3d[m] - coords3d[o] - v_dash = coords3d[n] - coords3d[p] - w_dash = coords3d[p] - coords3d[o] - u_norm = np.linalg.norm(u_dash) - v_norm = np.linalg.norm(v_dash) - w_norm = np.linalg.norm(w_dash) - u = u_dash / u_norm - v = v_dash / v_norm - w = w_dash / w_norm - phi_u = np.arccos(np.dot(u, w)) - phi_v = np.arccos(-np.dot(w, v)) - uxw = np.cross(u, w) - vxw = np.cross(v, w) - cos_dihed = np.dot(uxw, vxw)/(np.sin(phi_u)*np.sin(phi_v)) - - # Restrict cos_dihed to [-1, 1] - if cos_dihed >= 1 - cos_tol: - dihedral_rad = 0 - elif cos_dihed <= -1 + cos_tol: - dihedral_rad = np.arccos(-1) - else: - dihedral_rad = np.arccos(cos_dihed) - - if dihedral_rad != np.pi: - # wxv = np.cross(w, v) - # if wxv.dot(u) < 0: - if vxw.dot(u) < 0: - dihedral_rad *= -1 - return dihedral_rad - - -dihedral_grad = grad(calc_dihedral) diff --git a/deprecated/interpolate_extrapolate.py b/deprecated/interpolate_extrapolate.py deleted file mode 100644 index 7edb132518..0000000000 --- a/deprecated/interpolate_extrapolate.py +++ /dev/null @@ -1,53 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np - -from pysisyphus.optimizers.gdiis import gdiis, gediis -from pysisyphus.optimizers.poly_fit import poly_line_search - - -def interpolate_extrapolate(coords, energies, forces, steps, - ref_step=None, err_vecs=None, max_vecs=10, - gediis_thresh=1e-2, gdiis_thresh=2.5e-3): - - can_gediis = np.sqrt(np.mean(forces[-1]**2)) < gediis_thresh - can_diis = (ref_step is not None) and (np.sqrt(np.mean(ref_step**2)) < gdiis_thresh) - - # GDIIS check - if can_diis and (err_vecs is not None) and (ref_step is not None): - diis_result = gdiis(err_vecs, coords, forces, ref_step, max_vecs) - # GEDIIS check - elif can_gediis: - diis_result = gediis(coords, energies, forces) - else: - diis_result = None - - interpol_step = None - interpol_forces = None - interpol_energy = None - - if not diis_result or ((diis_result.name == "GDIIS") and (diis_result.N == 2)): - cur_energy = energies[-1] - prev_energy = energies[-2] - cur_grad = -forces[-1] - prev_grad = -forces[-2] - prev_step = steps[-1] - interpol_step, interpol_gradient, interpol_energy = poly_line_search( - cur_energy, prev_energy, - cur_grad, prev_grad, - prev_step, coords, - ) - if ((interpol_step is not None) - and (interpol_gradient is not None) - and (interpol_energy is not None)): - # prev_coords = coords[-2] - # new_coords = prev_coords + step - # geom.coords = new_coords - interpol_forces = -interpol_gradient - # interpol_step = step - elif diis_result: - interpol_forces = diis_result.forces - # geom.coords = diis_result.coords - # Set interpol step - import pdb; pdb.set_trace() - return interpol_step, interpol_forces, interpol_energy diff --git a/deprecated/optimizers/ANCOptimizer.py b/deprecated/optimizers/ANCOptimizer.py deleted file mode 100644 index 22fbc5208b..0000000000 --- a/deprecated/optimizers/ANCOptimizer.py +++ /dev/null @@ -1,49 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np - -from pysisyphus.optimizers.HessianOptimizer import HessianOptimizer - - -class ANCOptimizer(HessianOptimizer): - - def __init__(self, geometry, **kwargs): - super().__init__(geometry, **kwargs) - assert not self.is_cos and self.geometry.coord_type == "cart", \ - "ANCOptimizer can't be used with ChainOfStates-methods and " \ - "coordinate systems beside cartesians ('coord_type: cart')." - - def prepare_opt(self): - super().prepare_opt() - self.M_inv = self.geometry.mm_inv - - def optimize(self): - grad = self.geometry.gradient - self.forces.append(-grad.copy()) - self.energies.append(self.geometry.energy) - - H_mw = self.M_inv.dot(self.H).dot(self.M_inv) - H_mw_proj = self.geometry.eckart_projection(H_mw) - eigvals_mw, eigvecs_mw = np.linalg.eigh(H_mw_proj) - - # Neglect translational/rotational modes - keep = np.abs(eigvals_mw) > 1e-12 - eigvals_mw = eigvals_mw[keep] - eigvecs_mw = eigvecs_mw[:,keep] - - # Unweight mass-weighted normal modes - eigvecs = self.M_inv.dot(eigvecs_mw) - # Transform gradient - grad_q = eigvecs.T.dot(grad) - - if self.cur_cycle > 0: - self.update_hessian() - - dQ = -2*grad_q / (eigvals_mw + np.sqrt(eigvals_mw**2 + 4*(grad_q**2))) - - step = eigvecs.dot(dQ) - step_norm = np.linalg.norm(step) - - if step_norm > self.trust_radius: - step = step / step_norm * self.trust_radius - return step diff --git a/deprecated/optimizers/BFGS.py b/deprecated/optimizers/BFGS.py deleted file mode 100644 index 11911940b3..0000000000 --- a/deprecated/optimizers/BFGS.py +++ /dev/null @@ -1,121 +0,0 @@ -#!/usr/bin/env python3 - -import matplotlib.pyplot as plt -import numpy as np - -from pysisyphus.helpers import fit_rigid, procrustes -from pysisyphus.optimizers.BacktrackingOptimizer import BacktrackingOptimizer - -# [1] Nocedal, Wright - Numerical Optimization, 2006 - -class BFGS(BacktrackingOptimizer): - - def __init__(self, geometry, alpha=1.0, bt_force=20, **kwargs): - super(BFGS, self).__init__(geometry, alpha=alpha, - bt_force=bt_force, - **kwargs) - - self.eye = np.eye(len(self.geometry.coords)) - try: - self.inv_hessian = self.geometry.get_initial_hessian() - # ChainOfStates objects may not have get_initial_hessian - except AttributeError: - self.inv_hessian = self.eye.copy() - if (hasattr(self.geometry, "internal") - and (self.geometry.internal is not None)): - raise Exception("Have to add hessian projections etc.") - self.log("BFGS with align=True is somewhat broken right now, so " - "the images will be aligned only in the first iteration. " - ) - - def reset_hessian(self): - self.inv_hessian = self.eye.copy() - self.log("Resetted hessian") - - def prepare_opt(self): - if self.is_cos and self.align: - procrustes(self.geometry) - # Calculate initial forces before the first iteration - self.coords.append(self.geometry.coords) - self.forces.append(self.geometry.forces) - self.energies.append(self.geometry.energy) - - def scale_by_max_step(self, steps): - steps_max = np.abs(steps).max() - if steps_max > self.max_step: - fact = self.max_step / steps_max - """ - fig, ax = plt.subplots() - ax.hist(steps, bins=20)#"auto") - title = f"max(steps)={steps_max:.04f}, fact={fact:.06f}" - ax.set_title(title) - l1 = ax.axvline(x=self.max_step, c="k") - l2 = ax.axvline(x=-self.max_step, c="k") - ax.add_artist(l1) - ax.add_artist(l2) - fig.savefig(f"cycle_{self.cur_cycle:02d}.png") - plt.close(fig) - """ - steps *= self.max_step / steps_max - return steps - - def optimize(self): - last_coords = self.coords[-1] - last_forces = self.forces[-1] - last_energy = self.energies[-1] - - unscaled_steps = self.inv_hessian.dot(last_forces) - steps = self.scale_by_max_step(self.alpha*unscaled_steps) - - new_coords = last_coords + steps - self.geometry.coords = new_coords - - # Hessian rotation seems faulty right now ... - #if self.is_cos and self.align: - # (last_coords, last_forces, steps), _, self.inv_hessian = fit_rigid( - # self.geometry, - # (last_coords, - # last_forces, - # steps), - # hessian=self.inv_hessian) - - new_forces = self.geometry.forces - new_energy = self.geometry.energy - skip = self.backtrack(new_forces, last_forces, reset_hessian=True) - if skip: - self.reset_hessian() - self.geometry.coords = last_coords - #self.scale_alpha(unscaled_steps, self.alpha) - return None - - # Because we add the step later on we restore the original - # coordinates and set the appropriate energies and forces. - self.geometry.coords = last_coords - self.geometry.forces = new_forces - self.geometry.energy = new_energy - - self.forces.append(new_forces) - self.energies.append(new_energy) - # [1] Eq. 6.5, gradient difference, minus force difference - y = -(new_forces - last_forces) - sigma = new_coords - last_coords - # [1] Eq. 6.7, curvature condition - curv_cond = sigma.dot(y) - if curv_cond < 0: - self.log(f"curvature condition {curv_cond:.07} < 0!") - rho = 1.0 / y.dot(sigma) - if ((np.array_equal(self.inv_hessian, self.eye)) - # When align = True the above expression will evaluate to - # False. So we also check if we are in the first iteration. - or (self.cur_cycle == 0)): - # [1] Eq. 6.20, p. 143 - beta = y.dot(sigma)/y.dot(y) - self.inv_hessian = self.eye*beta - self.log(f"Using initial guess for inverse hessian, beta={beta}") - # Inverse hessian update - A = self.eye - np.outer(sigma, y) * rho - B = self.eye - np.outer(y, sigma) * rho - self.inv_hessian = (A.dot(self.inv_hessian).dot(B) - + np.outer(sigma, sigma) * rho) - - return steps diff --git a/deprecated/optimizers/LBFGS.py.backup b/deprecated/optimizers/LBFGS.py.backup deleted file mode 100644 index 60348734ba..0000000000 --- a/deprecated/optimizers/LBFGS.py.backup +++ /dev/null @@ -1,123 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np - -from pysisyphus.helpers import fit_rigid, procrustes -from pysisyphus.optimizers.Optimizer import Optimizer -from pysisyphus.optimizers.closures import bfgs_multiply - -# [1] Nocedal, Wright - Numerical Optimization, 2006 - - -class LBFGS(Optimizer): - def __init__(self, geometry, alpha=1.0, keep_last=15, - beta=1, **kwargs): - self.alpha = alpha - self.beta = beta - assert isinstance(keep_last, int) and keep_last > 0 - self.keep_last = keep_last - super().__init__(geometry, **kwargs) - - self.sigmas = list() - self.grad_diffs = list() - - def prepare_opt(self): - if self.is_cos and self.align: - procrustes(self.geometry) - # Calculate initial forces before the first iteration - self.coords.append(self.geometry.coords) - self.forces.append(self.geometry.forces) - self.energies.append(self.geometry.energy) - - def scale_by_max_step(self, steps): - steps_max = np.abs(steps).max() - step_norm = np.linalg.norm(steps) - self.log(f"Unscaled norm(step)={step_norm:.4f}") - if steps_max > self.max_step: - fact = self.max_step / steps_max - self.log(f"Scaling step with factor={fact:.4f}") - steps *= self.max_step / steps_max - step_norm = np.linalg.norm(steps) - self.log(f"Scaled norm(step)={step_norm:.4f}") - return steps - - def restrict_step_components(self, steps): - too_big = np.abs(steps) > self.max_step - self.log(f"Found {np.sum(too_big)} big step components.") - signs = np.sign(steps[too_big]) - # import pdb; pdb.set_trace() - steps[too_big] = signs * self.max_step - return steps - - def optimize(self): - prev_coords = self.coords[-1] - prev_forces = self.forces[-1] - - step = -bfgs_multiply(self.sigmas, self.grad_diffs, prev_forces, - beta=self.beta) - # step = self.alpha * self.restrict_step_components(step) - - # step = self.scale_by_max_step(step) - norm = np.linalg.norm(step) - self.log(f"unscaled norm(step)={norm:.4f}") - if norm > 0.1: - step = 0.1 * step / norm - # norm = np.linalg.norm(step) - # self.log(f"scaled norm(step)={norm:.4f}") - - - new_coords = prev_coords + self.alpha*step - - coords_tmp = prev_coords.copy() - forces_tmp = prev_forces.copy() - - self.geometry.coords = new_coords - if self.is_cos and self.align: - rot_vecs, rot_vec_lists, _ = fit_rigid( - self.geometry, - (prev_coords, prev_forces), - vector_lists=(self.sigmas, self.grad_diffs) - ) - prev_coords, prev_forces = rot_vecs - rot_sigmas, rot_grad_diffs = rot_vec_lists - np.testing.assert_allclose(np.linalg.norm(rot_sigmas), - np.linalg.norm(self.sigmas) - ) - np.testing.assert_allclose(np.linalg.norm(rot_grad_diffs), - np.linalg.norm(self.grad_diffs) - ) - self.sigmas = rot_sigmas - self.grad_diffs = rot_grad_diffs - - new_forces = self.geometry.forces - new_energy = self.geometry.energy - - sigma = new_coords - prev_coords - self.sigmas.append(sigma) - grad_diff = prev_forces - new_forces - self.grad_diffs.append(grad_diff) - - self.sigmas = self.sigmas[-self.keep_last:] - self.grad_diffs = self.grad_diffs[-self.keep_last:] - - # Because we add the step later on we restore the original - # coordinates and set the appropriate energies and forces. - self.geometry.coords = prev_coords - self.geometry.forces = new_forces - self.geometry.energy = new_energy - - self.forces.append(new_forces) - self.energies.append(new_energy) - - self.log("") - - return step - - def save_also(self): - return { - "alpha": self.alpha, - } - - def reset(self): - self.sigmas = list() - self.grad_diffs = list() diff --git a/deprecated/optimizers/LBFGS_mod.py b/deprecated/optimizers/LBFGS_mod.py deleted file mode 100644 index 59c27342b8..0000000000 --- a/deprecated/optimizers/LBFGS_mod.py +++ /dev/null @@ -1,133 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np - -from pysisyphus.helpers import fit_rigid, procrustes -from pysisyphus.optimizers.BacktrackingOptimizer import BacktrackingOptimizer -from pysisyphus.optimizers.closures import bfgs_multiply - -# [1] Nocedal, Wright - Numerical Optimization, 2006 - -import matplotlib.pyplot as plt - -def plot_steps_hist(steps, cycle, title): - fig, ax = plt.subplots() - ax.hist(steps) - fig.suptitle(f"Cycle {cycle:03d}, {title}") - fn = f"cycle_{cycle:03d}_{title}.png" - fig.savefig(fn) - plt.close() - - -def check_step(geom, steps, cycle): - imgs = geom.images - nimgs = len(imgs) - shape = (nimgs, -1, 3) - coords = geom.coords.reshape(*shape) - steps_ = steps.reshape(*shape) - ind = np.abs(steps_).argmax() - ind = np.unravel_index(ind, steps_.shape) - max_img = imgs[ind[0]] - fn = f"max_img_cycle_{cycle:03d}.xyz" - with open(fn, "w") as handle: - handle.write(max_img.as_xyz(ind)) - - -class LBFGS(BacktrackingOptimizer): - def __init__(self, geometry, alpha=1.0, keep_last=15, bt_force=20, **kwargs): - self.keep_last = keep_last - super().__init__(geometry, alpha=alpha, bt_force=bt_force, **kwargs) - - self.sigmas = list() - self.grad_diffs = list() - - def prepare_opt(self): - if self.is_cos and self.align: - procrustes(self.geometry) - # Calculate initial forces before the first iteration - self.coords.append(self.geometry.coords) - self.forces.append(self.geometry.forces) - self.energies.append(self.geometry.energy) - - def scale_by_max_step(self, steps): - steps_max = np.abs(steps).max() - step_norm = np.linalg.norm(steps) - self.log(f"Unscaled norm(step)={step_norm:.4f}") - # check_step(self.geometry, steps, self.cur_cycle) - # plot_steps_hist(steps, self.cur_cycle, "0_Unscaled") - if steps_max > self.max_step: - fact = self.max_step / steps_max - self.log(f"Scaling step with factor={fact:.4f}") - steps *= self.max_step / steps_max - step_norm = np.linalg.norm(steps) - self.log(f"Scaled norm(step)={step_norm:.4f}") - plot_steps_hist(steps, self.cur_cycle, "1_Scaled") - return steps - - def optimize(self): - prev_coords = self.coords[-1] - prev_forces = self.forces[-1] - - step = bfgs_multiply(self.sigmas, self.grad_diffs, prev_forces) - step = self.scale_by_max_step(step) - - new_coords = prev_coords + self.alpha*step - - coords_tmp = prev_coords.copy() - forces_tmp = prev_forces.copy() - - self.geometry.coords = new_coords - if self.is_cos and self.align: - rot_vecs, rot_vec_lists, _ = fit_rigid( - self.geometry, - (prev_coords, prev_forces), - vector_lists=(self.sigmas, self.grad_diffs) - ) - prev_coords, prev_forces = rot_vecs - # self.sigmas, self.grad_diffs = rot_vec_lists - rot_sigmas, rot_grad_diffs = rot_vec_lists - # if sigs: - # import pdb; pdb.set_trace() - np.testing.assert_allclose(np.linalg.norm(rot_sigmas), - np.linalg.norm(self.sigmas) - ) - np.testing.assert_allclose(np.linalg.norm(rot_grad_diffs), - np.linalg.norm(self.grad_diffs) - ) - self.sigmas = rot_sigmas - self.grad_diffs = rot_grad_diffs - - new_forces = self.geometry.forces - new_energy = self.geometry.energy - - skip = self.backtrack(new_forces, prev_forces) - print("alpha", self.alpha) - if skip: - self.geometry.coords = coords_tmp - return None - - sigma = new_coords - prev_coords - self.sigmas.append(sigma) - grad_diff = prev_forces - new_forces - self.grad_diffs.append(grad_diff) - - # if len(self.sigmas) == self.keep_last: - # import pdb; pdb.set_trace() - self.sigmas = self.sigmas[-self.keep_last:] - self.grad_diffs = self.grad_diffs[-self.keep_last:] - - # Because we add the step later on we restore the original - # coordinates and set the appropriate energies and forces. - self.geometry.coords = prev_coords - self.geometry.forces = new_forces - self.geometry.energy = new_energy - - self.forces.append(new_forces) - self.energies.append(new_energy) - - return step - - # def save_also(self): - # return { - # "alpha": self.alpha, - # } diff --git a/deprecated/optimizers/NaiveSteepestDescent.py b/deprecated/optimizers/NaiveSteepestDescent.py deleted file mode 100644 index 127d5e7b34..0000000000 --- a/deprecated/optimizers/NaiveSteepestDescent.py +++ /dev/null @@ -1,21 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np - -from pysisyphus.helpers import procrustes -from pysisyphus.optimizers.BacktrackingOptimizer import BacktrackingOptimizer - -class NaiveSteepestDescent(BacktrackingOptimizer): - - def __init__(self, geometry, **kwargs): - super(NaiveSteepestDescent, self).__init__(geometry, alpha=0.1, **kwargs) - - def optimize(self): - if self.is_cos and self.align: - procrustes(self.geometry) - - self.forces.append(self.geometry.forces) - - step = self.alpha*self.forces[-1] - step = self.scale_by_max_step(step) - return step diff --git a/deprecated/optimizers/ONIOMOpt.py b/deprecated/optimizers/ONIOMOpt.py deleted file mode 100644 index a5daf3b5a3..0000000000 --- a/deprecated/optimizers/ONIOMOpt.py +++ /dev/null @@ -1,115 +0,0 @@ -import numpy as np - -from pysisyphus.optimizers.Optimizer import Optimizer -from pysisyphus.optimizers.LBFGS import LBFGS -from pysisyphus.optimizers.closures import small_lbfgs_closure -from pysisyphus.optimizers.restrict_step import scale_by_max_step -from pysisyphus.helpers_pure import highlight_text - - -class ONIOMOpt(Optimizer): - def __init__( - self, - geometry, - *args, - micro_cycles=None, - max_micro_cycles=50, - control_step=False, - max_step=0.2, - step="full", - **kwargs, - ): - super().__init__(geometry, max_step=max_step, **kwargs) - - self.max_micro_cycles = max_micro_cycles - self.control_step = control_step - self.step = step - assert self.step in ("full", "high") - - layers = self.geometry.layers - assert len(layers) == 2, "Only ONIOM2 supported yet!" - - # Set up micro cycles for every layer - if micro_cycles is None: - micro_cycles = np.ones(len(layers), dtype=int) - micro_cycles[0] = 0 - self.micro_cycles = micro_cycles - self.log(f"Micro cycles: {self.micro_cycles}") - - self.lbfgs_closure = small_lbfgs_closure(history=10, gamma_mult=False) - self.high_steps = list() - - (self.real_model,), (self.high_model,) = self.geometry.layers - # Freeze high layer, but also freeze atoms in real layer that become - # link atoms in the high layer. - link_inds = [link.parent_ind for link in self.high_model.links] - self.freeze_in_real = self.high_model.atom_inds + link_inds - self.freeze_in_high = [2, 1, 3] - print("!!! HARDCODED self.freeze_in_high !!!") - - self.micros_converged = 0 - self.micro_opt_cycles = list() - - def optimize(self): - - ####################### - # Relax real geometry # - ####################### - - coords3d_org = self.geometry.coords3d.copy() - real_geom = self.real_model.as_geom(self.geometry.atoms, coords3d_org.copy()) - real_geom.freeze_atoms = self.freeze_in_real - - key = "real" - micro_cycles = self.micro_cycles[0] - if micro_cycles == 0: - micro_cycles = self.max_micro_cycles - real_opt_kwargs = { - "prefix": key, - "h5_group_name": f"{key}_opt", - "max_cycles": micro_cycles, - "thresh": self.thresh, # Respect parents convergence threshold - "line_search": True, - "align": False, - } - real_opt = LBFGS(real_geom, **real_opt_kwargs) - print("\n" + highlight_text(f"Opt Cycle {self.cur_cycle}, Micro cycles") + "\n") - real_opt.run() - real_step = real_geom.coords3d - coords3d_org - self.micros_converged += real_opt.is_converged - self.micro_opt_cycles.append(real_opt.cur_cycle + 1) - print("\n" + highlight_text(f"Micro cycles finished") + "\n") - - ####################### - # Relax full geometry # - ####################### - - # Calculate full ONIOM forces with previously releaxed, real coordinates - results = self.geometry.get_energy_and_forces_at(real_geom.coords3d.flatten()) - energy = results["energy"] - forces = results["forces"] - self.energies.append(energy) - self.forces.append(forces.copy()) - - try: - prev_step = self.steps[-1] + real_step.flatten() - except IndexError: - prev_step = None - if self.step == "high": - forces.reshape(-1, 3)[self.freeze_in_high] = 0.0 - step = self.lbfgs_closure(forces, prev_step=prev_step) - - if self.control_step: - step = scale_by_max_step(step, self.max_step) - step += real_step.flatten() - return step - - def postprocess_opt(self): - tot_micro_cycs = sum(self.micro_opt_cycles) - msg = ( - f"\nMicro-cycle optimizations:\n" - f"\t Attempted: {self.cur_cycle+1}\n" - f"\t Converged: {self.micros_converged}\n" - f"\t Total cycles: {tot_micro_cycs}\n" - ) - self.log(msg) diff --git a/deprecated/optimizers/RSAlgorithm.py b/deprecated/optimizers/RSAlgorithm.py deleted file mode 100644 index 8b4458f7e2..0000000000 --- a/deprecated/optimizers/RSAlgorithm.py +++ /dev/null @@ -1,115 +0,0 @@ -#!/usr/bin/env python3 - -# [1] The Importance of Step Control in Optimization Methods - -from math import sqrt - -import numpy as np -from scipy.optimize import root_scalar - -from pysisyphus.optimizers.HessianOptimizer import HessianOptimizer - - -class RSAlgorithm(HessianOptimizer): - - def optimize(self): - g = self.geometry.gradient - self.forces.append(-self.geometry.gradient.copy()) - self.energies.append(self.geometry.energy) - - if self.cur_cycle > 0: - self.update_trust_radius() - self.update_hessian() - - H = self.H - if self.geometry.internal: - H = self.geometry.internal.project_hessian(H) - - vals, vecsT = np.linalg.eigh(H) - # Exclude small eigenvalues and corresponding -vectors - # small_inds = np.abs(vals) < 1e-6 - small_inds = np.abs(vals) < 1e-10 - self.log(f"Found {small_inds.sum()} small eigenvalues.") - neg_inds = vals < 0 - neg = neg_inds.sum() - - # vals = vals[~small_inds] - # vecsT = vecsT[:,~small_inds] - g_ = vecsT.T.dot(g) - self.log(f"shape(g_)={g.shape}, shape(vals)={vals.shape}, shape(vecsT)={vecsT.shape}") - - def get_step(lambda_): - """Returns step for a given lambda""" - # _ = -g_/(vals+lambda_) - # TODO: remove offset - _ = -g_/(vals+lambda_) - return vecsT.dot(_) - - if neg == 0: - self.log("Hessian is positive definite.") - self.log("Checking pure QN-step ... ") - step = get_step(0) - step_norm = np.linalg.norm(step) - if step_norm <= self.trust_radius: - self.log(f"norm(step)={step_norm:.6f} is fine") - predicted_change = step.dot(g) + 0.5 * step.dot(self.H).dot(step) - self.predicted_energy_changes.append(predicted_change) - return step - self.log(f"norm(QN-step)={step_norm:.6f} is too big.") - else: - # Hessian is not positive definite - self.log("Hessian is not positive definite.") - smallest_eigval = vals[0] - self.log(f"Smallest eigenvalue is {smallest_eigval:.6f}") - - def on_sphere_linear(lambda_): - return 1/self.trust_radius - 1/np.linalg.norm(get_step(lambda_)) - - _b1 = -vals[0] - x0 = _b1 + 1e-3 - x1 = x0 + 1e-3 - - # Defining a bracket using infinity (float("inf")) doesn't seem to work. - # Instead we use a big number. - upper_bracket = 1e10 - # Hessian is positive definite - if neg == 0: - bracket = [0, upper_bracket] - # Hessian has negative eigenvalues, is not positive definitie - else: - bracket = [_b1+1e-6, upper_bracket] - sol = root_scalar(on_sphere_linear, x0=x0, x1=x1, bracket=bracket) - if not sol.converged: - raise Exception("Root search did not converge!") - lambda_ = sol.root - # Check shifted hessian for positive definiteness by adding the shift - # to the smallest eigenvalue and see if this is > 0. If so we can use this - # step (Case II). - if vals[0] + lambda_ > 0: - step = get_step(lambda_) - step_norm = np.linalg.norm(step) - self.log(f"Found valid step with λ={lambda_:.6f} and norm={step_norm:.6f}") - predicted_change = step.dot(g) + 0.5 * step.dot(self.H).dot(step) - self.predicted_energy_changes.append(predicted_change) - return step - - import pdb; pdb.set_trace() - self.log(f"Shifted hessian (λ={lambda_:.6f} is not positive definite!") - self.log("Determining new step using second parameter τ.") - # Shifted hessian is not positive definite (Case III). - lambda_ = vals[0] - # frac_sum = np.sum(g_[1:] / (vals[1:] - vals[0])) - frac_sum = np.sum(g_[1:] / (vals[1:] - lambda_)) - tau = sqrt(self.trust_radius**2 - frac_sum**2) - self.log(f"τ={tau:.6f}") - - # The second term is still in the eigensystem of the hessian, whereas the - # first term is in the original space. So this won't work ... - raise Exception("first term is in original space, second term is "\ - "is still in eigenspace of hessian.") - step = get_step(lambda_) + tau*g_[:,0] - - predicted_change = step.dot(g) + 0.5 * step.dot(self.H).dot(step) - self.predicted_energy_changes.append(predicted_change) - - return step diff --git a/deprecated/optimizers/RSRFOptimizer.py b/deprecated/optimizers/RSRFOptimizer.py deleted file mode 100644 index 022eac8ba8..0000000000 --- a/deprecated/optimizers/RSRFOptimizer.py +++ /dev/null @@ -1,154 +0,0 @@ -#!/usr/bin/env python3 - -# See [1] https://pubs.acs.org/doi/pdf/10.1021/j100247a015 -# Banerjee, 1985 -# [2] https://aip.scitation.org/doi/abs/10.1063/1.2104507 -# Heyden, 2005 -# [3] https://onlinelibrary.wiley.com/doi/abs/10.1002/jcc.540070402 -# Baker, 1985 -# [4] 10.1007/s002140050387 -# Bofill, 1998, Restricted-Step-RFO -# [5] https://link.springer.com/article/10.1007/s00214-016-1847-3 -# Birkholz, 2016 - -import numpy as np - -from pysisyphus.optimizers.HessianOptimizer import HessianOptimizer - - -class RSRFOptimizer(HessianOptimizer): - """Optimizer to find first-order saddle points.""" - - rfo_dict = { - "min": (0, "min"), - "max": (-1, "max"), - } - - def __init__(self, geometry, max_micro_cycles=50, **kwargs): - super().__init__(geometry, **kwargs) - - self.max_micro_cycles = int(max_micro_cycles) - assert max_micro_cycles >= 1 - - self.alpha0 = 1 - self.alpha_max = 1e8 - - def solve_rfo(self, rfo_mat, kind="min"): - # So if I use eig instead of eigh here it even works ... - # my bad, ahhh! The unscaled RFO matrix may be symmetric, - # but the scaled ones aren't anymore. - eigenvalues, eigenvectors = np.linalg.eig(rfo_mat) - eigenvalues = eigenvalues.real - eigenvectors = eigenvectors.real - sorted_inds = np.argsort(eigenvalues) - - # Depending on wether we want to minimize (maximize) along - # the mode(s) in the rfo mat we have to select the smallest - # (biggest) eigenvalue and corresponding eigenvector. - first_or_last, verbose = self.rfo_dict[kind] - ind = sorted_inds[first_or_last] - # Given sorted eigenvalue-indices (sorted_inds) use the first - # (smallest eigenvalue) or the last (largest eigenvalue) index. - step_nu = eigenvectors.T[ind] - nu = step_nu[-1] - self.log(f"nu_{verbose}={nu:.4e}") - # Scale eigenvector so that its last element equals 1. The - # final is step is the scaled eigenvector without the last element. - step = step_nu[:-1] / nu - eigval = eigenvalues[ind] - self.log(f"eigenvalue_{verbose}={eigval:.4e}") - return step, eigval, nu - - def optimize(self): - forces = self.geometry.forces - self.forces.append(forces) - self.energies.append(self.geometry.energy) - - if self.cur_cycle > 0: - self.update_trust_radius() - self.update_hessian() - - H = self.H - if self.geometry.internal: - H = self.geometry.internal.project_hessian(self.H) - eigvals, eigvecs = np.linalg.eigh(H) - - # Transform to eigensystem of hessian - forces_trans = eigvecs.T.dot(forces) - - # Minimize energy along all modes - min_mat = np.asarray(np.bmat(( - (np.diag(eigvals), -forces_trans[:,None]), - (-forces_trans[None,:], [[0]]) - ))) - - alpha = self.alpha0 - min_diag_indices = np.diag_indices(eigvals.size) - for mu in range(self.max_micro_cycles): - assert alpha > 0, "alpha should not be negative" - self.log(f"RS-RFO micro cycle {mu:02d}, alpha={alpha:.6f}") - # We only have to update one eigenvalue - min_mat_scaled = min_mat.copy() - min_mat_scaled[min_diag_indices] /= alpha - min_mat_scaled[:-1,-1] /= alpha - rfo_step, eigval_min, nu_min = self.solve_rfo(min_mat_scaled, "min") - - # As of Eq. (8a) of [4] max_eigval and min_eigval also - # correspond to: - # eigval_min_ = -forces_trans.dot(rfo_step) - # np.testing.assert_allclose(eigval_min, eigval_min_) - - # Create the full PRFO step - rfo_norm = np.linalg.norm(rfo_step) - self.log(f"rfo_norm={rfo_norm:.6f}") - - inside_trust = rfo_norm < self.trust_radius + 1e-3 - if inside_trust: - self.log("step is inside trust radius. breaking.") - break - elif alpha > self.alpha_max: - print("alpha > alpha_max. breaking.") - break - - # Derivative of the squared step w.r.t. alpha - tval = 2*eigval_min/(1+rfo_norm**2 * alpha) - numer = forces_trans**2 - denom = (eigvals - eigval_min * alpha)**3 - quot = np.sum(numer / denom) - self.log(f"quot={quot:.6f}") - dstep2_dalpha = (2*eigval_min/(1+rfo_norm**2 * alpha) - * np.sum(forces_trans**2 - / ((eigvals - eigval_min * alpha)**3) - ) - ) - self.log(f"analytic deriv.={dstep2_dalpha:.6f}") - # Update alpha - alpha_step = (2*(self.trust_radius*rfo_norm - rfo_norm**2) - / dstep2_dalpha - ) - self.log(f"alpha_step={alpha_step:.4f}") - alpha += alpha_step - self.log("") - - # Right now the step is still given in the Hessians eigensystem. We - # transform it back now. - step = eigvecs.dot(rfo_step) - step_norm = np.linalg.norm(step) - # This would correspond to "pure" RFO without the iterative - # step-restriction. Here we will just scale down the step, if it - # is too big. - if self.max_micro_cycles == 1 and step_norm > self.trust_radius: - self.log("Scaled down step") - step = step / step_norm * self.trust_radius - step_norm = np.linalg.norm(step) - - self.log(f"norm(step)={np.linalg.norm(step):.6f}") - - # Calculating the energy change from eigval_min and nu_min seems to give - # big problems. - # predicted_energy_change = 1/2 * eigval_min / nu_min**2 - predicted_change = step.dot(-forces) + 0.5 * step.dot(self.H).dot(step) - self.predicted_energy_changes.append(predicted_change) - - self.log("") - return step diff --git a/deprecated/optimizers/SciPyOptimizer.py b/deprecated/optimizers/SciPyOptimizer.py deleted file mode 100644 index 6b26d1f328..0000000000 --- a/deprecated/optimizers/SciPyOptimizer.py +++ /dev/null @@ -1,79 +0,0 @@ -#!/usr/bin/env python3 - -import time - -import numpy as np -from scipy.optimize import minimize - -from pysisyphus.helpers import check_for_stop_sign -from pysisyphus.optimizers.Optimizer import Optimizer - - -class StopOptException(Exception): - pass - - -class SciPyOptimizer(Optimizer): - - def __init__(self, geometry, method="l-bfgs-b", **kwargs): - super(SciPyOptimizer, self).__init__(geometry, **kwargs) - - if self.align: - print("Ignoring align in SciPyOptimizer.") - self.method = method - self.options = { - "disp": True, - "maxiter": self.max_cycles, - "gtol": 1e-3, - } - - def callback(self, xk): - self.cur_cycle += 1 - forces = self.geometry.forces - step = self.coords[-1] - xk - - self.steps.append(step) - - if self.is_cos: - self.tangents.append(self.geometry.get_tangents()) - - self.check_convergence() - - if self.dump: - self.write_cycle_to_file() - - if self.is_zts: - self.geometry.reparametrize() - - if check_for_stop_sign(): - raise StopOptException() - - def fun(self, coords): - start_time = time.time() - - self.coords.append(self.geometry.coords) - self.geometry.coords = coords - forces = self.geometry.forces - self.forces.append(forces) - self.energies.append(self.geometry.energy) - forces_rms = np.sqrt(np.mean(np.square(forces))) - - end_time = time.time() - elapsed_seconds = end_time - start_time - self.cycle_times.append(elapsed_seconds) - - # gradient = -forces - return forces_rms, -forces - - def run(self): - # self.print_header() - x0 = self.geometry.coords - try: - self.opt_res = minimize(self.fun, x0, jac=True, method=self.method, - callback=self.callback, options=self.options) - if self.opt_res.success: - print("Converged!") - else: - print("Didn't converge.") - except StopOptException: - self.log("Caught StopOptException. Stopping.") diff --git a/deprecated/optimizers/SphereOptimizer.py b/deprecated/optimizers/SphereOptimizer.py deleted file mode 100644 index 4edfb44ad9..0000000000 --- a/deprecated/optimizers/SphereOptimizer.py +++ /dev/null @@ -1,156 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np -from scipy.optimize import minimize - -from pysisyphus.optimizers.Optimizer import Optimizer - - -class SphereOptimizer(Optimizer): - - def __init__(self, geometry, radius=0.3, **kwargs): - super().__init__(geometry, **kwargs) - - self.radius = radius - # self.initial_coords = self.geometry.coords.copy() - self.initial_coords = np.array((0.05, 0, 0)) - self.ref_ind = 1 - - def optimize(self): - forces = self.geometry.forces - self.forces.append(forces) - self.energies.append(self.geometry.energy) - - ref_force = forces[self.ref_ind] - cur_coords = self.geometry.coords - ref_diff = cur_coords[self.ref_ind] - self.initial_coords[self.ref_ind] - constr_forces = ( - forces - ref_force*(cur_coords - self.initial_coords) / ref_diff - ) - direction = constr_forces / np.linalg.norm(constr_forces) - - step = 0.01*direction - - return step - - -def run(): - import matplotlib.pyplot as plt - from matplotlib.patches import Circle - from pysisyphus.calculators.CerjanMiller import CerjanMiller - from pysisyphus.calculators.AnaPot import AnaPot - - ref_coords = np.array((0.05, 0, 0)) - geom = CerjanMiller.get_geom(ref_coords) - # geom = AnaPot.get_geom((-1, 1, 0)) - - ref_ind = 1 - free_inds = np.arange(geom.coords.size) != ref_ind - free_ref_coords = ref_coords[free_inds] - ref_coord = ref_coords[ref_ind] - - def get_constr_coord(coords, radius): - free_coords = coords[free_inds] - _ = max(0, radius**2 - radius*np.sum((free_coords - free_ref_coords)**2)) - sqrt = np.sqrt(_) - return ref_coord + sqrt - - def get_forces_mod(forces, coords): - ref_force = forces[ref_ind] - constr_coord = coords[ref_ind] - return forces - ref_force*(coords-ref_coords)/(constr_coord-ref_coord) - - dR = 0.01 - alpha = 0.01 - R = dR - # def wrapper(coords): - # geom.coords = coords - # constr_coord = get_constr_coord(geom.coords, R) - # diff = R - np.linalg.norm(geom.coords - ref_coords) - # geom.set_coord(ref_ind, constr_coord) - # forces = geom.forces - # forces_mod = get_forces_mod(forces, geom.coords) - # rms = np.sqrt(np.mean(forces_mod**2)) - # return rms, -forces_mod - - # res = minimize(wrapper, geom.coords, jac=True) - # print(res) - # return - - dR = 0.01 - alpha = 0.01 - R = dR - all_coords = list() - radii = [R, ] - for i in range(10): - all_coords.append(geom.coords.copy()) - print(f"Cycle {i:02d}, R={R:.04f}") - constr_coord = get_constr_coord(geom.coords, R) - - # Set constrained coordinate - geom.set_coord(ref_ind, constr_coord) - assert geom.coords[ref_ind] == constr_coord - all_coords.append(geom.coords.copy()) - - diff = R - np.linalg.norm(geom.coords - ref_coords) - - # Optimize remaining coordinates - forces = geom.forces - norm_forces = np.linalg.norm(forces) - - # Force acting on the constrained coordinate - ref_force = forces[ref_ind] - forces_mod_ = forces - ref_force*(geom.coords-ref_coords)/(constr_coord-ref_coord) - forces_mod = get_forces_mod(forces, geom.coords) - np.testing.assert_allclose(forces_mod_, forces_mod) - norm_forces_mod = np.linalg.norm(forces_mod) - import pdb; pdb.set_trace() - - print(f"\tnorm(f)={norm_forces:.6f} " - f"norm(fm)={norm_forces_mod:.6f} dR={diff:.6f}" - ) - # if norm_forces_mod < alpha: - # # Increase radius - # R += dR - # radii.append(R) - - # # Start extrapolation if enough points are present - # if len(radii) < 2: - # continue - # V = (all_coords[-1] - all_coords[-2]) / (radii[-1] - radii[-2]) - # import pdb; pdb.set_trace() - # while True: - # print("extrapol") - # extrapolated_coords = geom.coords + V*dR - # geom.coords = extrapolated_coords - # forces = geom.forces - # forces_norm = np.linalg.norm(forces) - # forces_mod = get_forces_mod(forces, geom.coords) - # norm_force_mod = np.linalg.norm(forces_mod) - # if norm_forces_mod > alpha: - # break - - import pdb; pdb.set_trace() - dir_ = forces_mod / np.linalg.norm(forces_mod) - step = 0.01 * dir_ - new_coords = geom.coords + step - geom.coords = new_coords - - pot = geom.calculator - pot.plot() - all_coords = np.array(all_coords) - radii = np.array(radii) - pot.ax.plot(all_coords[:,0], all_coords[:,1], "o-") - pot.ax.scatter(ref_coords[0], ref_coords[1], s=20, c="r") - for radius in radii: - circle = Circle(ref_coords[:2], radius=radius, fill=False) - pot.ax.add_artist(circle) - for i, xy in enumerate(all_coords[:,:2]): - pot.ax.annotate(i, xy) - pot.ax.set_xlim(-0.2, 0.2) - pot.ax.set_ylim(-0.2, 0.2) - plt.show() - - -if __name__ == "__main__": - run() diff --git a/deprecated/optimizers/line_search.py b/deprecated/optimizers/line_search.py deleted file mode 100644 index 71802c168b..0000000000 --- a/deprecated/optimizers/line_search.py +++ /dev/null @@ -1,100 +0,0 @@ -#!/usr/bin/env python3 - -import numpy as np - -from pysisyphus.optimizers.line_searches import wolfe - - -def run_line_search(f, df, get_p, x0, alpha_0=1, alpha_max=1): - x = x0 - alpha_prev = None - gradients = list() - step_dirs = list() - - for i in range(50): - f_0 = f(x) - grad = np.array(df(x)) - norm = np.linalg.norm(grad) - print(f"{i:02d} norm(grad)={norm:.6f}") - if norm < 1e-4: - print("Converged!") - break - p = get_p(x) - # p = -grad / np.linalg.norm(grad) - - gradients.append(grad) - step_dirs.append(p) - - alpha_guess = 1 - if alpha_prev: - # Try an improved guess for alpha - # See [1], between Eq. (3.59) and (3.60), "Initial step length" - numer = gradients[-2].dot(step_dirs[-2]) - denom = gradients[-1].dot(step_dirs[-1]) - alpha_guess = alpha_prev * numer/denom - # Restrict the maximum value of the guessed alpha. Otherwise - # huge alphas may result when the gradient reduction was big in - # the previous cycle. - alpha_guess = min(alpha_max, alpha_guess) - print(f"\tusing alpha_guess={alpha_guess:.6f}") - assert alpha_guess > 0 - - # f_evals_prev = f_evals - # df_evals_prev = df_evals - # import pdb; pdb.set_trace() - - kwargs = { - "f": f, - "df": df, - "x0": x, - "p": p, - "f0": f_0, - "g0": grad, - "alpha_init": alpha_guess, - } - alpha, f_, df_ = wolfe(**kwargs) - - # alpha, f_, df_ = wolfe(f, df, x, p, f_0, df_0=grad, alpha=alpha_guess) - # fc_ = f_evals - f_evals_prev - # gc_ = df_evals - df_evals_prev - # print(f"\talpha={alpha:.6f}, fc={fc_}, gc={gc_}") - print(f"\talpha={alpha:.6f}") - # sp_alpha, fc, gc, *_ = sp_line_search(f_, f_grad_, x, p) - # if sp_alpha: - # print(f"\tsp_alpha={sp_alpha:.6f}, fc={fc}, gc={gc}") - # x = x + sp_alpha*p - # else: - # x = x + alpha*p - x = x + alpha*p - alpha_prev = alpha - - -def run(): - f_evals = 0 - df_evals = 0 - def f(x): - nonlocal f_evals - f_evals += 1 - return 2*x**4 + 5*x**3 - 2*x**2 + 10*x - - def f_grad(x): - nonlocal df_evals - df_evals += 1 - return (8*x**3 + 15*x**2 - 4*x + 10, ) - - def f_(x): - return f(*x) - - def f_grad_(x): - return np.array(f_grad(*x)) - - def get_p(x): - grad = f_grad_(x) - return -grad / np.linalg.norm(grad) - - x0 = (-3, ) - run_line_search(f_, f_grad_, get_p, x0) - - -if __name__ == "__main__": - run() diff --git a/deprecated/optimizers/line_searches.py b/deprecated/optimizers/line_searches.py deleted file mode 100644 index 6e5a03036b..0000000000 --- a/deprecated/optimizers/line_searches.py +++ /dev/null @@ -1,489 +0,0 @@ -# [1] Nocedal - Numerical Optimization, Second ed. - -import logging - -import numpy as np - - -logger = logging.getLogger("optimizer") -def log(msg): - logger.debug(msg) - - -def interpol_alpha_quad(f_0, df_0, f_alpha_0, alpha_0): - return -df_0*alpha_0**2 / 2 / (f_alpha_0 - f_0 - df_0*alpha_0) - - -def interpol_alpha_cubic(f_0, df_0, f_alpha_0, f_alpha_1, alpha_0, alpha_1): - quot = 1 / (alpha_0**2 * alpha_1**2 * (alpha_1 - alpha_0)) - A = np.array(((alpha_0**2, -alpha_1**2), - (-alpha_0**3, alpha_1**3))) - B = np.array(((f_alpha_1 - f_0 - df_0*alpha_1), - (f_alpha_0 - f_0 - df_0*alpha_0))) - a, b = quot * A @ B - alpha_cubic = (-b + (b**2 - 3*a*df_0)**(0.5)) / (3*a) - return alpha_cubic - - -class LineSearchConverged(Exception): - - def __init__(self, alpha): - self.alpha = alpha - - -class LineSearchNotConverged(Exception): - pass - - -def linesearch_wrapper(cond): - def cond_wrapper(func): - def wrapper(f, df, x0, p, f0=None, g0=None, c1=0.1, c2=0.9, max_cycles=10, - *args, **kwargs): - alpha_fs = {} - def _phi(alpha): - alpha = float(alpha) - try: - f_alpha = alpha_fs[alpha] - except KeyError: - log(f"\tEvaluating energy for alpha={alpha:.6f}") - f_alpha = f(x0 + alpha*p) - alpha_fs[alpha] = f_alpha - return f_alpha - - alpha_gs = {} - dphis = {} - def _dphi(alpha): - alpha = float(alpha) - try: - df_alpha = alpha_gs[alpha] - dphi_ = df_alpha @ p - except KeyError: - log(f"\tEvaluating gradient for alpha={alpha:.6f}") - df_alpha = df(x0 + alpha*p) - alpha_gs[alpha] = df_alpha - dphi_ = df_alpha @ p - dphis[alpha] = dphi_ - return dphi_ - - def get_phi_dphi(what, alpha, check=True): - """Wrapper that handles function/gradient evaluations.""" - alpha = float(alpha) - whats = "f g fg".split() - assert what in whats - calc_funcs = { - "f": _phi, - "g": _dphi, - } - result = [calc_funcs[w](alpha) for w in what] - # Check if we got both phi and dphi for alpha now. If so we - # can check if the chosen condition (Wolfe/approx. Wolfe) is - # satisfied. - if check and (alpha > 0.0) \ - and (alpha in alpha_fs) and (alpha in alpha_gs) and cond_func(alpha): - raise LineSearchConverged(alpha) - # Dont return a list if only f or g was requested. - if len(what) == 1: - result = result[0] - return result - - def get_fg(what, alpha): - """Lookup raw function/gradient values at alpha.""" - whats = "f g fg".split() - assert what in whats - lookups = { - "f": alpha_fs, - "g": alpha_gs, - } - result = [lookups[w][alpha] for w in what] - if len(what) == 1: - result = result[0] - return result - - if f0 is None: - phi0 = get_phi_dphi("f", 0) - else: - phi0 = f0 - alpha_fs[0.] = f0 - if g0 is None: - dphi0 = get_phi_dphi("g", 0) - else: - dphi0 = g0 @ p - alpha_gs[0.] = g0 - - def sufficiently_decreased(alpha): - """Sufficient decrease/Armijo condition.""" - return _phi(alpha) <= (phi0 + c1 * alpha * dphi0) - - def curvature_condition(alpha): - return _dphi(alpha) >= c2*dphi0 - - def strong_curvature_condition(alpha): - return abs(_dphi(alpha)) <= -c2*dphi0 - - def wolfe_condition(alpha): - """Normal, not strong, Wolfe condition.""" - return sufficiently_decreased(alpha) \ - and curvature_condition(alpha) - - def strong_wolfe_condition(alpha): - """Strong wolfe condition.""" - return sufficiently_decreased(alpha) \ - and strong_curvature_condition(alpha) - - conds = { - "armijo": sufficiently_decreased, - "curv": curvature_condition, - "wolfe": wolfe_condition, - "strong_wolfe": strong_wolfe_condition, - } - - cond_func = conds[cond] - - # Q_prev = 0 - # C_prev = 0 - # print(f"\talpha_init={alpha_init:.6f}, ak={ak:.6f}, bk={bk:.6f}") - # approx_permanently = False - # conds = { - # # True: ("approx.", t2_condition), - # True: ("approx.", approx_wolfe_condition), - # False: ("wolfe", wolfe_condition), - # } - # if f_prev: - # Q = 1 + Q_prev * Delta - # C = C_prev + (abs(alpha_fs[0.]) - C_prev)/Q - # use_approx = abs(f0 - f_prev) <= omega*C - # if approx_permanently or use_approx: - # approx_permanently = True - # else: - # use_approx = False - # cond_name, cond = conds[use_approx] - # # print(f"{k:02d}: [{ak:.6f},{bk:.6f}]") - # # if wolfe_condition(ak) or t2_condition(ak): - # print(f"Using {cond_name} condition") - # cond = wolfe_condition - # cond = t2_condition - # cond = approx_wolfe_condition - - linesearch_result = func(x0, p, get_phi_dphi, get_fg, conds, - max_cycles, *args, **kwargs) - return linesearch_result - return wrapper - return cond_wrapper - - -@linesearch_wrapper("wolfe") -def hager_zhang(x0, p, get_phi_dphi, get_fg, conds, max_cycles, - alpha_init=None, alpha_prev=None, - f_prev=None, dphi0_prev=None, quad_step=False, - eps=1e-6, theta=0.5, gamma=0.5, rho=5, - psi_0=.01, psi_1=.1, psi_2=2., psi_low=0.1, psi_hi=10, - Delta=.7, omega=1e-3, max_bisects=10): - epsk = eps * abs(get_fg("f", 0.)) - phi0, dphi0 = get_phi_dphi("fg", 0.) - f0, g0 = get_fg("fg", 0.) - - cond = conds["wolfe"] - - import pdb; pdb.set_trace() - def bisect(a, b): - """Bisect interval [a, b].""" - for i in range(max_bisects): - # U3 a. - d = (1 - theta)*a + theta*b - dphi_d = get_phi_dphi("g", d) - if dphi_d >= 0: - return a, d - - phi_d = get_phi_dphi("f", d) - # U3 b. - # If (dphi_d > 0) we would already have returned above... - if phi_d <= phi0 + epsk: - a = d - # U3 c. - elif phi_d > phi0 + epsk: - b = d - raise Exception("Bisect failed!") - - def interval_update(a, b, c): - """Narrows down the bracketing interval.""" - # U0 - if not (a < c < b): - return a, b - - phi_c, dphi_c = get_phi_dphi("fg", c) - # U1, sign of slope projection changed. We already passed the minimum. - if dphi_c >= 0: - return a, c - # U2, we are moving towards the minimum. - elif phi_c <= phi0 + epsk: - return c, b - - # U3, phi_c increased above phi0, so we probably passed the minimum. - return bisect(a, c) - - def secant(a, b): - """Take secant step.""" - dphia = get_phi_dphi("g", a) - dphib = get_phi_dphi("g", b) - return (a*dphib - b*dphia) / (dphib - dphia) - - def double_secant(a, b): - """Take secant² step.""" - c = secant(a, b) - A, B = interval_update(a, b, c) - cB_close = np.isclose(c, B) - cA_close = np.isclose(c, A) - - if cB_close: - c_dash = secant(b, B) - elif cA_close: - c_dash = secant(a, A) - - if cB_close or cA_close: - a_dash, b_dash = interval_update(A, B, c_dash) - else: - a_dash, b_dash = A, B - return a_dash, b_dash - - def bracket(c): - """Generate initial interval [a, b] that satisfies the opposite - slope condition (dphi(a) < 0, dphi(b) > 0). - """ - cs = list() - for j in range(10): - cs.append(c) - - dphi_j = get_phi_dphi("g", c) - - if (dphi_j >= 0) and (j == 0): - return 0, c - - phi_j = get_phi_dphi("f", c) - if dphi_j >= 0: - phi_inds = np.array([get_fg("f", c) for c in cs[:-1]]) <= (phi0 + epsk) - # See https://stackoverflow.com/a/8768734 - ci = len(phi_inds) - phi_inds[::-1].argmax() - 1 - return cs[ci], c - elif phi_j > (phi0 + epsk): - return bisect(0, c) - - c *= rho - - def norm_inf(arr): - """Returns infinity norm of given array.""" - return np.linalg.norm(arr, np.inf) - - def initial(): - """Get an initial guess for alpha.""" - if (~np.isclose(x0, np.zeros_like(x0))).any(): - c = psi_0 * norm_inf(x0)/norm_inf(g0) - elif not np.isclose(f0, 0): - c = psi_0 * f0 / norm_inf(g0)**2 - else: - c = 1 - return c - - def take_quad_step(alpha, g0_): - """Try to get alpha for minimum step from quadratic interpolation.""" - import pdb; pdb.set_trace() - fact = max(psi_low, g0_/(dphi0*psi_2)) - alpha_ = min(fact, psi_hi) * alpha - phi_ = get_phi_dphi("f", alpha_) - denom = 2*((phi_-phi0)/alpha_ - dphi0) - f_temp = get_fg("f", alpha_) - if denom > 0.: - c = -dphi0*alpha_ / denom - if f_temp > get_fg("f", 0): - c = max(c, alpha_*1e-10) - else: - c = alpha - return c - - if alpha_init is None and alpha_prev: - alpha_init = alpha_prev - if alpha_init is None and alpha_prev is None: - alpha_init = initial() - - # Put everything in a try/except block because now everytime - # we evaluate phi/dphi at some alpha and both phi and dphi - # are present for this alpha, e.g. from a previous calculation, - # convergence of the linesearch will be checked, and - # LineSearchConverged may be raised. Using exceptions enables - # us to also return from nested functions. - try: - if quad_step: - g0_ = -2*abs(get_fg("f", 0)/alpha_init) if (dphi0_prev is None) \ - else dphi0_prev - alpha_init = take_quad_step(psi_2*alpha_init, g0_) - # This may raise LineSearchConverged - _ = get_phi_dphi("fg", alpha_init) - - # TODO: cubic interpolation for better alpha_init - ak, bk = bracket(alpha_init) - for k in range(max_cycles): - if cond(ak): - break - # secant² step - a, b = double_secant(ak, bk) - if (b - a) > gamma*(bk - ak): - # Bisection step - c = (a + b)/2 - a, b = interval_update(a, b, c) - ak, bk = a, b - except LineSearchConverged as lsc: - ak = lsc.alpha - - f_new, g_new = get_fg("fg", ak) - return ak, f_new, g_new, dphi0 - - -@linesearch_wrapper("armijo") -def backtracking(x0, p, get_phi_dphi, get_fg, conds, max_cycles, - alpha_init=1., rho_lo=5e-2, rho_hi=0.9): - """Backtracking line search enforcing Armijo conditions. - - Uses only energy evaluations. - - See [1], Chapter 3, Line Search methods, Section 3.1 p. 31 and - Section 3.5 p. 56.""" - cond = conds["armijo"] - - log("Starting backtracking line search") - phi0, dphi0 = get_phi_dphi("fg", 0) - - alpha_prev = None - alpha = alpha_init - for i in range(max_cycles): - phi_i = get_phi_dphi("f", alpha) - log(f"\tCycle {i:02d}: alpha={alpha:.6f}, ϕ={phi_i:.6f}") - - if cond(alpha): - log(f"\tLine search converged after {i} cycles.") - break - - if i == 0: - # Quadratic interpolation - alpha_new = interpol_alpha_quad(phi0, dphi0, phi_i, alpha) - type_ = "Quadratic" - else: - # Cubic interpolation - phi_prev = get_phi_dphi("f", alpha_prev) - alpha_new = interpol_alpha_cubic(phi0, dphi0, phi_prev, phi_i, alpha_prev, alpha) - type_ = "Cubic" - log(f"\tNew alpha from {type_}: {alpha_new:.6f}") - - lower_bound = alpha * rho_lo - upper_bound = alpha * rho_hi - if alpha_new < lower_bound: - log("\tNew alpha is too big!") - if alpha_new > upper_bound: - log("\tNew alpha is too high!") - - # Assert that alpha doesn't change too much compared to the previous alpha - alpha_new = min(alpha_new, upper_bound) - alpha_new = max(alpha_new, lower_bound) - alpha_prev = alpha - alpha = alpha_new - log(f"\tAlpha for next cycles: {alpha:.6f}\n") - else: - raise LineSearchNotConverged - - return alpha - - -@linesearch_wrapper("wolfe") -def wolfe(x0, p, get_phi_dphi, get_fg, conds, max_cycles, - alpha_init=1., alpha_min=0.01, alpha_max=100., fac=2): - """Wolfe line search. - - Uses only energy & gradient evaluations. - - See [1], Chapter 3, Line Search methods, Section 3.5 p. 60.""" - - phi0, dphi0 = get_phi_dphi("fg", 0) - - def zoom(alpha_lo, alpha_hi, phi_lo, - phi_alpha_=None, alpha_0_=None, max_cycles=10): - - alphas = list() - phi_alphas = list() - if phi_alpha_: - phi_alphas = [phi_alpha_, ] - if alpha_0_: - alphas = [alpha_0_, ] - - for j in range(max_cycles): - # Interpoaltion of alpha between alpha_lo, alpha_hi - # - # Try cubic interpolation if at least two additional alphas and - # corresponding phi_alpha values are available beside alpha = 0. - if len(phi_alphas) > 1: - alpha_prev = alphas[-1] - phi_alpha_prev = phi_alphas[-1] - alpha_j = interpol_alpha_cubic(phi0, dphi0, - phi_alpha_, phi_alpha_prev, - alpha_0_, alpha_prev - ) - # Try quadratic interpolation if at one additional alpha and - # corresponding phi_alpha value is available beside alpha = 0. - elif len(phi_alphas) == 1: - alpha_j = interpol_alpha_quad(phi0, dphi0, phi_alpha_, alpha_0_) - # Fallback to simple bisection - else: - alpha_j = (alpha_lo + alpha_hi) / 2 - - phi_j = get_phi_dphi("f", alpha_j) - # Store the values so they can be reused for cubic interpolation - alphas.append(alpha_j) - phi_alphas.append(phi_j) - - # True if alpha is still too big or if the function value - # increased compared to the previous cycle. - if (not conds["armijo"](alpha_j) or phi_j > phi_lo): - # Shrink interval to (alpha_lo, alpha_j) - alpha_hi = alpha_j - continue - - dphi_j = get_phi_dphi("g", alpha_j) - if conds["curv"](alpha_j): - print(f"\tzoom converged after {j+1} cycles.") - return alpha_j - - if (dphi_j * (alpha_hi - alpha_lo)) >= 0: - alpha_hi = alpha_lo - # Shrink interval to (alpha_j, alpha_hi) - alpha_lo = alpha_j - raise Exception("zoom() didn't converge in {j+1} cycles!") - - alpha_prev = 0 - phi_prev = phi0 - if alpha_init is not None: - alpha_i = alpha_init - # This does not seem to help - # elif f_0_prev is not None: - # alpha_i = min(1.01*2*(f_0 - f_0_prev) / dphi_0, 1.) - # print("ai", alpha_i) - # alpha_i = 1. if alpha_i < 0. else alpha_i - else: - alpha_i = 1.0 - - try: - for i in range(10): - phi_i = get_phi_dphi("f", alpha_i) - if (not conds["armijo"](alpha_i) or ((phi_i >= phi_prev) and i > 0)): - zoom(alpha_prev, alpha_i, phi_prev, phi_i, alpha_i) - - dphi_i = get_phi_dphi("g", alpha_i) - if conds["curve"](alpha_i): - raise LineSearchConverged(alpha_i) - - if dphi_i >= 0: - zoom(alpha_i, alpha_prev, phi_i, phi_alpha_=phi_i, alpha_0_=alpha_i) - prev_alpha = alpha_i - alpha_i = min(fac * alpha_i, alpha_max) - else: - raise LineSearchNotConverged - except LineSearchConverged as lsc: - alpha = lsc.alpha - - return alpha diff --git a/deprecated/overlaps/Overlapper.py b/deprecated/overlaps/Overlapper.py deleted file mode 100644 index d7fe3ad330..0000000000 --- a/deprecated/overlaps/Overlapper.py +++ /dev/null @@ -1,351 +0,0 @@ -#!/usr/bin/env python3 - -import itertools as it -import logging -import os -from pathlib import Path -import re - -from natsort import natsorted -import numpy as np - -from pysisyphus.calculators.Gaussian09 import Gaussian09 -from pysisyphus.calculators.Gaussian16 import Gaussian16 -from pysisyphus.calculators.ORCA import ORCA -from pysisyphus.calculators.Turbomole import Turbomole -from pysisyphus.helpers import (geom_from_xyz_file, index_array_from_overlaps, - np_print -) - - -class Overlapper: - orca_exts = ("out", "gbw", "cis") - gaussian_exts = ("log", "fchk", "dump_635r") - - logger = logging.getLogger("overlapper") - - def __init__(self, path, ovlp_with="previous", prev_n=0, - calc_key=None, calc_kwargs=None): - self.path = Path(path) - self.ovlp_with = ovlp_with - assert ovlp_with in ("previous", "first") - self.calc_key = calc_key - self.calc_kwargs = calc_kwargs - self.calc_kwargs["out_dir"] = path - - mobj = re.match("previous(\d+)", self.ovlp_with) - self.prev_n = prev_n - assert self.prev_n >= 0 - - self.setter_dict = { - "gaussian09": self.set_g16_files, - "gaussian16": self.set_g16_files, - "orca": self.set_orca_files, - "turbomole": self.set_turbo_files, - } - self.files_from_dir_dict = { - "orca": self.set_orca_files_from_dir, - "gaussian09": self.set_gaussian16_files_from_dir, - "gaussian16": self.set_gaussian16_files_from_dir, - } - - def log(self, message, lvl="info"): - func = getattr(self.logger, lvl) - func(message) - - def keyfunc(self, element): - regex = "_(\d+)\.(\d+)\." - mobj = re.search(regex, element) - return tuple([int(num) for num in mobj.groups()]) - - def discover_files(self, path): - image_str = "image_" - calc_str = "calculator_" - files = [str(f) for f in path.glob(image_str + "*")] - if len(files) > 1: - base_str = image_str - else: - files = [str(f) for f in path.glob(calc_str + "*")] - base_str = calc_str - print(f"Found {len(files)} files starting with '{base_str}'. " - f"I assume that base string is '{base_str}'.") - files = sorted(files, key=self.keyfunc) - files_dict = dict() - for key, elements in it.groupby(files, self.keyfunc): - files_dict[key] = list(elements) - return files_dict - - def discover_files_in_dir(self, path, exts): - files_list = list() - for ext in exts: - glob = f"*{ext}" - fns = [_ for _ in path.glob(glob)] - assert len(fns) == 1, f"Searched for *.{ext} and was expecting " \ - f"one file but found {len(fns)} files " \ - f"instead: {fns}" - fn = str(fns[0]) - files_list.append(fn) - return files_list - - def discover_geometries(self, path): - xyz_fns = natsorted(path.glob("*.xyz")) - geoms = [geom_from_xyz_file(xyz_fn) for xyz_fn in xyz_fns] - self.restore_calculators(geoms) - - return geoms - - def file_by_ext(self, iterable, ext): - matches = [f for f in iterable if f.endswith(ext)] - if len(matches) == 0: - raise Exception(f"Couldn't file with extension '{ext}'!") - assert len(matches) == 1 - return matches[0] - - def set_files_on_calculator(self, geom, files_dict, calc_class, exts, - calc_number, cycle_number=0): - key = (calc_number, cycle_number) - files = files_dict[key] - calc = calc_class(calc_number=calc_number, **self.calc_kwargs) - geom.set_calculator(calc) - print(f"Setting files on calculator_{calc_number:03d}:") - for ext in exts: - file_ext = self.file_by_ext(files, ext) - setattr(calc, ext, file_ext) - print(f"\t{file_ext}") - - def set_files_on_calculators(self, geoms, files_dict, calc_class, - exts): - for i, geom in enumerate(geoms): - calc_number, cycle_number = i, 0 - self.set_files_on_calculator(geom, files_dict, calc_class, exts, - calc_number, cycle_number) - - def set_files_from_dir(self, geom, path, calc_number): - func = self.files_from_dir_dict[self.calc_key] - func(geom, path, calc_number) - - def set_orca_files_from_dir(self, geom, path, calc_number): - exts = self.orca_exts - files_list = self.discover_files_in_dir(path, exts) - files_dict = { - (calc_number, 0): files_list, - } - self.set_files_on_calculator(geom, files_dict, ORCA, exts, calc_number) - geom.calculator.store_overlap_data(geom.atoms, geom.coords) - - def set_orca_files(self, geoms, files_dict): - self.set_files_on_calculators(geoms, files_dict, ORCA, self.orca_exts) - for geom in geoms: - geom.calculator.store_overlap_data(geom.atoms, geom.coords) - - def set_gaussian16_files_from_dir(self, geom, path, calc_number): - exts = self.gaussian_exts - - files_list = self.discover_files_in_dir(path, exts) - log_file, *files_list = files_list - files_dict = { - (calc_number, 0): files_list, - } - exts_without_log = exts[1:] - assert "log" not in exts_without_log - self.set_files_on_calculator(geom, files_dict, Gaussian16, - exts_without_log, - calc_number - ) - log_path = Path(log_file) - nmos, roots = geom.calculator.parse_log(log_path) - calc = geom.calculator - calc.nmos = nmos - calc.roots = roots - calc.store_overlap_data(geom.atoms, geom.coords) - - def set_g16_files(self, geoms, files_dict): - exts = ("fchk", "dump_635r") - self.set_files_on_calculators(geoms, files_dict, Gaussian16, exts) - - first_log = Path(self.file_by_ext(files_dict[(0, 0)], ".log")) - nmos, roots = geoms[0].calculator.parse_log(first_log) - for geom in geoms: - calc = geom.calculator - calc.nmos = nmos - calc.roots = roots - calc.store_overlap_data(geom.atoms, geom.coords) - - def set_turbo_files(self, geoms, files_dict): - exts = ("mos", "ciss_a", "out", "control") - self.set_files_on_calculators(geoms, files_dict, Turbomole, exts) - - for geom in geoms: - calc = geom.calculator - if hasattr(calc, "ciss_a"): - calc.td_vec_fn = calc.ciss_a - elif hasattr(calc, "ccres"): - calc.td_vec_fn = calc.ccres - calc.store_overlap_data(geom.atoms, geom.coords) - - def restore_calculators(self, geoms): - files_dict = self.discover_files(self.path) - unique_calculators = set([calc_num for calc_num, cycle_num in files_dict]) - # assert len(unique_calculators) <= len(geoms), ("Number of discovered " - # f"unique calculators ({len(unique_calculators)}) is bigger than the " - # f"number of discovered geometries ({len(geoms)})." - # ) - print(f"Found {len(unique_calculators)} unique calculators.") - print(f"Found {len(geoms)} geometries.") - calc_num = min(len(unique_calculators), len(geoms)) - setter_func = self.setter_dict[self.calc_key] - setter_func(geoms[:calc_num], files_dict) - print(f"Restored {calc_num} calculators.") - return calc_num - - def similar_overlaps(self, overlaps_for_state, ovlp_thresh=.1, diff_thresh=.2): - """Return True if overlaps for a state are similar.""" - # Find overlaps above ovlp_thresh - above_inds = np.where(np.abs(overlaps_for_state) > ovlp_thresh)[0] - # Unambiguous assignment. There is a one to one correspondence between - # the states. - if len(above_inds) == 1: - return False - # Given the a full row containing overlaps this may evaluate to True if - # something went wrong and the overlaps ARE that small. Given only a subset - # of a full row, e.g. when only the first N states are considered this may - # evaluate to True if the index of the current state lies below N. E.g. if we - # check state 6, but got only the overlaps from state 1 to 5. - elif len(above_inds) == 0: - return False - - above_thresh = np.abs(overlaps_for_state[above_inds]) - max_ovlp_ind = above_thresh.argmax() - max_ovlp = above_thresh[max_ovlp_ind] - without_max = np.delete(above_thresh, max_ovlp_ind) - # Consider the differences between the maximum overlap and the smaller ones. - diffs = np.abs(max_ovlp - without_max) - # Return True if any difference is below the threshold - return any(diffs < diff_thresh) - - def get_ovlp_func(self, ovlp_type, double_mol=False, recursive=False, - consider_first=None): - def wf_ovlp(calc1, calc2, ao_ovlp): - ovlp_mats = calc1.wfow.overlaps_with(calc2.wfow, ao_ovlp=ao_ovlp) - ovlp_mat = ovlp_mats[0] - return ovlp_mat - - def tden_ovlp(calc1, calc2, ao_ovlp): - return calc1.tdens_overlap_with_calculator(calc2, - ao_ovlp=ao_ovlp) - ovlp_dict = { - "wf": wf_ovlp, - "tden": tden_ovlp, - } - valid_ovlps = "/".join([str(k) for k in ovlp_dict.keys()]) - assert ovlp_type in ovlp_dict.keys(), \ - f"Invalid ovlp_type! Valid keys are {valid_ovlps}." - - ovlp_func_ = ovlp_dict[ovlp_type] - - def ovlp_func(geoms, i, j, depth=2, ao_ovlp=None): - ith_geom = geoms[i] - jth_geom = geoms[j] - ith_calc = geoms[i].calculator - jth_calc = geoms[j].calculator - icn = ith_calc.calc_number - jcn = jth_calc.calc_number - if double_mol: - true_ovlp_mat_fn = f"ao_ovlp_true_{icn:03d}_{jcn:03d}" - try: - ao_ovlp = np.loadtxt(true_ovlp_mat_fn) - self.logger.info(f"Using true AO overlaps from {true_ovlp_mat_fn}.") - except: - self.logger.info("Doing double molecule calculation to get " - "AO overlaps." - ) - ao_ovlp = jth_geom.calc_double_ao_overlap(ith_geom) - np.savetxt(f"ao_ovlp_true_{icn:03d}_{jcn:03d}", ao_ovlp) - self.log(f"Calculationg overlaps for steps {icn:03d} and {jcn:03d}.") - ovlp_mat = ovlp_func_(ith_calc, jth_calc, ao_ovlp) - - self.log(ovlp_mat) - - ovlp_mat_fn = f"{ovlp_type}_ovlp_mat_{icn:03d}_{jcn:03d}" - np.savetxt(self.path / ovlp_mat_fn, ovlp_mat) - - similar = any( - [self.similar_overlaps(per_state) - for per_state in ovlp_mat[:,:consider_first]] - ) - if similar: - self.log( "Some entries of the overlap matrix between steps " - f"{icn:03d} and {jcn:03d} are very similar!") - if recursive and similar and (i > 0) and depth > 0: - self.log(f"Comparing {icn-1:03d} and {jcn:03d} now, " - f"because steps {icn:03d} and {jcn:03d} were " - "too similar." - ) - return ovlp_func(geoms, i-1, j, depth-1) - return ovlp_mat - - return ovlp_func - - @np_print - def overlaps_for_geoms(self, geoms, ovlp_type="wf", double_mol=False, - recursive=False, consider_first=None, skip=0): - # if skip > 0 and recursive: - # raise Exception("recursive = True and skip > 0 can't be used " - # "together." - # ) - ovlp_func = self.get_ovlp_func(ovlp_type, double_mol, recursive, - consider_first) - - if double_mol: - assert hasattr(geoms[0].calculator, "run_double_mol_calculation"), \ - "Double molecule calculation not implemented for " \ - f"{self.calc_key}." - - self.log(f"Doing {ovlp_type.upper()}-overlaps.") - - inds_list = list() - ovlp_mats = list() - is_similar = lambda ovlp_mat: any([self.similar_overlaps(per_state) - for per_state in ovlp_mat[:,:consider_first]] - ) - for i in range(len(geoms)-1): - # We can be sure that i is always a valid index. - j = i+(1+skip) - if self.ovlp_with == "first": - i = 0 - elif self.prev_n: - i = max(i - self.prev_n, 0) - if j >= len(geoms): - break - ovlp_mat = ovlp_func(geoms, i, j) - ovlp_mats.append(ovlp_mat) - index_array = index_array_from_overlaps(ovlp_mat) - inds_list.append(index_array) - self.log(index_array) - inds_arr = np.array(inds_list) - ovlp_mats = np.array(ovlp_mats) - max_ovlp_inds_fn = f"{ovlp_type}_max_ovlp_inds" - ovlp_mats_fn = f"{ovlp_type}_ovlp_mats" - np.savetxt(self.path / max_ovlp_inds_fn, inds_arr, fmt="%i") - np.save(ovlp_mats_fn, ovlp_mats) - self.log("") - self.log("Max overlap indices.") - for i, row in enumerate(inds_arr): - self.log(f"Step {i:02d}: {row}") - - -if __name__ == "__main__": - # path = Path("/scratch/programme/pysisyphus/tests_staging/test_diabatizer/cb3_def2svp") - path = Path("/scratch/programme/pysisyphus/tests_staging/test_diabatizer/cb3_def2svp/first_five") - calc_kwargs = { - "keywords": "CAM-B3LYP def2-SVP RIJCOSX D3BJ TightSCF", - "blocks": "%tddft nroots 5 tda false end %maxcore 1000", - "track": True, - "pal": 4, - } - calc_key = "orca" - ovl = Overlapper(path, calc_key, calc_kwargs) - geoms = ovl.discover_geometries(path) - # files_dict = dia.discover_files(path) - # dia.restore_calculators("orca") - ovl.overlaps(geoms) diff --git a/deprecated/overlaps/__init__.py b/deprecated/overlaps/__init__.py deleted file mode 100644 index c8df99aec9..0000000000 --- a/deprecated/overlaps/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -import logging -import sys - - -logger = logging.getLogger("overlapper") -logger.setLevel(logging.DEBUG) -# delay = True prevents creation of empty logfiles -handler = logging.FileHandler("overlapper.log", mode="w", delay=True) -fmt_str = "%(message)s" -formatter = logging.Formatter(fmt_str) -handler.setFormatter(formatter) -logger.addHandler(handler) -# Prints to stdout -logger.addHandler(logging.StreamHandler(sys.stdout)) diff --git a/deprecated/overlaps/couplings.py b/deprecated/overlaps/couplings.py deleted file mode 100644 index f1276c3b7d..0000000000 --- a/deprecated/overlaps/couplings.py +++ /dev/null @@ -1,141 +0,0 @@ -#!/usr/bin/env python3 - -import itertools as it - -import matplotlib.pyplot as plt -import numpy as np -from scipy.interpolate import interp1d - - -from pysisyphus.overlaps.sorter import sort_cut - -np.set_printoptions(suppress=True, precision=2) - -def interpolate(x, ys, x_fine, **kwargs): - if x is None: - x = np.arange(ys.size) - f = interp1d(x, ys, **kwargs) - y_fine = f(x_fine) - return y_fine - - -def get_state_couplings(ad_ens, dia_ens, ind_a, ind_b, x=None): - assert ind_a != ind_b - - if x is None: - x = np.arange(ad_ens.shape[0]) - x_fine = np.linspace(x.min(), x.max(), 512) - interp = lambda ys: interpolate(x=x, ys=ys, x_fine=x_fine) - a_ad = ad_ens[:,ind_a] - b_ad = ad_ens[:,ind_b] - a_ad_fine = interp(a_ad) - b_ad_fine = interp(b_ad) - ad_fine = interpolate(x, dia_ens, x_fine, axis=0) - - a_dia = dia_ens[:,ind_a] - b_dia = dia_ens[:,ind_b] - a_dia_fine = interp(a_dia) - b_dia_fine = interp(b_dia) - dia_fine = interpolate(x, dia_ens, x_fine, axis=0) - - frac = np.abs( - (a_dia_fine - b_ad_fine) / (a_ad_fine - b_ad_fine) - ) - # frac_root = np.full_like(frac) - frac_root = np.sqrt(frac) - # Cap at 1 - frac_root[frac_root > 1] = 1 - alpha = np.arccos(frac_root) - # print(frac_root) - # print(frac_root.max()) - Vd = np.abs( - (b_ad_fine - a_ad_fine) * np.cos(alpha) * np.sin(alpha) - ) - - grey = "#aaaaaa" - - fig, (ax_ad, ax_dia, ax_Vd) = plt.subplots(nrows=3) - title = f"Couplings between State {ind_a+1} and {ind_b+1}" - ax_ad.plot(x_fine, ad_fine, c=grey) - ax_ad.plot(x_fine, a_ad_fine) - ax_ad.plot(x_fine, b_ad_fine) - ax_ad.set_title("adiabatic") - - ax_dia.plot(x_fine, dia_fine, c=grey) - ax_dia.plot(x_fine, a_dia_fine) - ax_dia.plot(x_fine, b_dia_fine) - ax_dia.set_title("diabatic") - - ax_Vd.plot(x_fine, Vd) - ax_Vd.set_title("couplings") - - fig.suptitle(title) - - # plt.tight_layout() - fig.tight_layout(rect=[0, 0.03, 1, 0.95]) - - plt.show() - - -def run(): - # ens from parse_tddft.py - ens = np.loadtxt("all_energies.dat") - # Drop GS - ens = ens[:,1:] - - # fig, ax = plt.subplots() - # ax.plot(ens, "o-") - # plt.show() - - max_ovlp_inds = np.loadtxt("tden_max_ovlp_inds", dtype=int) - # print("max_overlap_inds") - # print(max_ovlp_inds) - - consider_first = 3 - ens_sorted, inds_sorted = sort_cut(ens, max_ovlp_inds, - consider_first=consider_first) - - # fig, ax = plt.subplots() - # ax.plot(ens_sorted, "o-") - # plt.show() - - get_state_couplings(ens, ens_sorted, 2, 1) - #get_state_couplings(ens, ens_sorted, 1, 2) - get_state_couplings(ens, ens_sorted, 2, 0) - # get_state_couplings(ens, ens_sorted, 0, 2) - - -def d2d3(): - adia = np.loadtxt("d2d3_adia.csv") - dia = np.loadtxt("d2d3_dia.csv") - x = adia[:,0] - adia = adia[:,1:] - dia = dia[:,1:] - get_state_couplings(adia, dia, 0, 1, x=x) - # get_state_couplings(adia, dia, 1, 0, x=x) - get_state_couplings(adia, dia, 1, 2, x=x) - # get_state_couplings(adia, dia, 2, 1, x=x) - - -def couplings(states): - energies = np.loadtxt("all_energies.dat") - # Drop GS - energies = energies[:,1:] - max_ovlp_inds = np.loadtxt("tden_max_ovlp_inds", dtype=int) - combs = it.combinations(states, 2) - # coupling_mat = [get_state_couplings - consider_first = max(states) - energies_sorted, inds_sorted = sort_cut(energies, max_ovlp_inds, - consider_first=consider_first) - # fig, ax = plt.subplots() - # ax.plot(ens_sorted, "o-") - # plt.show() - state_couplings = { - (ind_a, ind_b): get_state_couplings( - energies, energies_sorted, ind_a, ind_b - ) for ind_a, ind_b in combs - } - -if __name__ == "__main__": - run() - # d2d3() diff --git a/deprecated/overlaps/sorter.py b/deprecated/overlaps/sorter.py deleted file mode 100644 index 8a056c9fcb..0000000000 --- a/deprecated/overlaps/sorter.py +++ /dev/null @@ -1,71 +0,0 @@ -#!/usr/bin/env python3 - -import logging - -import numpy as np - - -def sort_cut(cut, max_overlap_inds, consider_first=None): - # For N-1 overlaps we have N points - assert max_overlap_inds.shape[0] == cut.shape[0]-1 - if not consider_first: - consider_first = cut.shape[1] - cut_sorted = cut.copy() - cur_inds = np.arange(cut.shape[1]) - # Start from index 1, as we keep the first row - all_inds = [cur_inds, ] - for i, inds in enumerate(max_overlap_inds, 1): - jumps = [(j, s) for j, s in enumerate(inds) if s != j] - - unique_inds = np.unique(inds[:consider_first]) - # if jumps: - # print(f"from {i-1:02d} to {i:02d}", jumps) - - new_inds = cur_inds.copy() - if unique_inds.size != consider_first: - print(f"Step {i:02d}, indices are non-unique! Not skipping!") - print(f"\t ({i-1} -> {i}): {inds}") - else: - for j, s in jumps: - new_inds[j] = cur_inds[s] - - cur_inds = new_inds - cut_sorted[i:,cur_inds] = cut[i:] - all_inds.append(cur_inds) - all_inds = np.array(all_inds) - return cut_sorted, all_inds - - -def load_ascii_or_bin(arr_fn): - try: - arr = np.loadtxt(arr_fn) - return arr - except UnicodeDecodeError as err: - msg = f"{arr_fn} seems to be binary. Trying to load it." - print(msg) - # logging.exception(f"{arr_fn} seems to be binary. Trying to load it.") - - arr = np.load(arr_fn, ) - return arr - - -def sort_by_overlaps(energies_fn, max_ovlp_inds_fn, consider_first=None): - energies = load_ascii_or_bin(energies_fn) - max_ovlp_inds = load_ascii_or_bin(max_ovlp_inds_fn).astype(int) - print("Maximum overlap inds") - print(max_ovlp_inds) - # Drop GS - energies = energies[:,1:] - assert max_ovlp_inds.shape[0] == energies.shape[0]-1 - energies_sorted, inds_sorted = sort_cut(energies, max_ovlp_inds, - consider_first=consider_first) - ens_sorted_fn = "energies_sorted" - np.savetxt(f"{ens_sorted_fn}.dat", energies_sorted) - np.save(ens_sorted_fn, energies_sorted) - inds_sorted_fn = "all_inds_sorted" - np.savetxt(f"{inds_sorted_fn}.dat", inds_sorted, fmt="%d") - np.save(inds_sorted_fn, inds_sorted) - print() - print("Indices, sorted") - for i, row in enumerate(inds_sorted): - print(f"Step {i:02d}", row) diff --git a/deprecated/plot.py b/deprecated/plot.py deleted file mode 100644 index abd98191f6..0000000000 --- a/deprecated/plot.py +++ /dev/null @@ -1,900 +0,0 @@ -#!/usr/bin/env python3 - -import argparse -import os -from pathlib import Path -import sys - -import h5py -import matplotlib -from matplotlib.animation import FuncAnimation -from matplotlib.patches import Rectangle -import matplotlib.image as mpimg -import matplotlib.pyplot as plt -import numpy as np -from scipy.interpolate import splrep, splev -import yaml - -from pysisyphus.constants import AU2KJPERMOL, BOHR2ANG, AU2EV -from pysisyphus.cos.NEB import NEB -from pysisyphus.Geometry import Geometry -from pysisyphus.peakdetect import peakdetect -from pysisyphus.wrapper.jmol import render_cdd_cube - - -CDD_PNG_FNS = "cdd_png_fns" - - -class Plotter: - def __init__(self, coords, data, ylabel, interval=750, save=None, - legend=None): - self.coords = coords - self.data = data - self.ylabel = ylabel - self.interval = interval - self.save = save - self.legend = legend - - # First image of the first cycle - self.anchor = self.coords[0][0] - self.cycles = len(self.data) - self.pause = True - - self.fig, self.ax = plt.subplots() - self.fig.canvas.mpl_connect('key_press_event', self.on_keypress) - - self.ax.set_xlabel("Path length / Bohr") - y_min = self.data.min() - y_max = self.data.max() - self.ax.set_ylim(y_min, y_max) - self.ax.set_ylabel(self.ylabel) - - self.coord_diffs = self.get_coord_diffs(self.coords) - - if self.data.ndim == 2: - self.update_func = self.update_plot - elif self.data.ndim == 3: - self.update_func = self.update_plot2 - - def get_coord_diffs(self, coords, normalize=False): - coord_diffs = list() - for per_cycle in coords: - tmp_list = [0, ] - for i in range(len(per_cycle)-1): - diff = np.linalg.norm(per_cycle[i+1]-per_cycle[i]) - tmp_list.append(diff) - tmp_list = np.cumsum(tmp_list) - offset = np.linalg.norm(self.anchor-per_cycle[0]) - tmp_list += offset - if normalize: - tmp_list /= tmp_list.max() - coord_diffs.append(tmp_list) - return np.array(coord_diffs) - - def update_plot(self, i): - """Use this when only 1 state is present.""" - self.fig.suptitle("Cycle {}".format(i)) - self.lines[0].set_xdata(self.coord_diffs[i]) - self.lines[0].set_ydata(self.data[i]) - if self.save: - self.save_png(i) - - def update_plot2(self, i): - """Use this when several states are present.""" - self.fig.suptitle("Cycle {}".format(i)) - for j, line in enumerate(self.lines): - line.set_ydata(self.data[i][:, j]) - if self.save: - self.save_png(i) - - def save_png(self, frame): - frame_fn = f"step{frame}.png" - if not os.path.exists(frame_fn): - self.fig.savefig(frame_fn) - - def animate(self): - self.lines = self.ax.plot(self.coord_diffs[0], self.data[0], "o-") - if self.legend: - self.ax.legend(self.lines, self.legend) - self.animation = FuncAnimation( - self.fig, - self.update_func, - frames=self.cycles, - interval=self.interval - ) - if self.save: - self.animation.save("animation.gif", writer='imagemagick', fps=5) - plt.show() - - def on_keypress(self, event): - """Pause on SPACE press.""" - #https://stackoverflow.com/questions/41557578 - if event.key == " ": - if self.pause: - self.animation.event_source.stop() - else: - self.animation.event_source.start() - self.pause = not self.pause - - -def plot_energies(): - keys = ("energy", "cart_coords") - (energies, coords), num_cycles, num_images = load_results(keys) - - if isinstance(num_images, list): - print("Please use --aneb instead of --energies") - return - - lengths = np.array([len(e) for e in energies]) - equal_lengths = lengths == lengths[-1] - # Hack to support growing string calculations - energies = np.array([e for e, l in zip(energies, equal_lengths) if l]) - coords = np.array([c for c, l in zip(coords, equal_lengths) if l]) - num_cycles, num_images = energies.shape - - energies -= energies.min() - energies *= AU2KJPERMOL - - # Static plot of path with equally spaced images - fig, ax = plt.subplots() - colors = matplotlib.cm.Greys(np.linspace(.2, 1, num=num_cycles)) - for cycle, color in zip(energies, colors): - ax.plot(cycle, "o-", color=color) - ax.set_title("Energies") - - kwargs = { - "ls": ":", - "color": "darkgrey", - } - try: - last_cycle = energies[-1] - spl = splrep(np.arange(num_images), last_cycle) - # Calculate interpolated values - x2 = np.linspace(0, num_images, 100) - y2 = splev(x2, spl) - # Only consider maxima - peak_inds, _ = peakdetect(y2, lookahead=2) - if not peak_inds: - ax.plot(x2, y2) - else: - peak_inds = np.array(peak_inds)[:, 0].astype(int) - peak_xs = x2[peak_inds] - peak_ys = y2[peak_inds] - ax.plot(x2, y2, peak_xs, peak_ys, "x") - for px, py in zip(peak_xs, peak_ys): - ax.axhline(y=py, **kwargs) - line = matplotlib.lines.Line2D([px, px], [0, py], **kwargs) - ax.add_line(line) - except TypeError: - print("Not enough images for splining!") - - # Always draw a line at the minimum y=0 - ax.axhline(y=0, **kwargs) - ax.set_xlabel("Image") - ax.set_ylabel("dE / kJ mol⁻¹") - - fig2, ax2 = plt.subplots() - last_energies = energies[-1].copy() - xs = np.arange(len(last_energies)) - ax2.plot(xs, last_energies, "o-") - ax2.set_xlabel("Image") - ax2.set_ylabel("$\Delta$E / kJ mol⁻¹") - ax2.set_title(f"Cycle {len(energies)-1}") - - first_image_en = last_energies[0] - last_image_en = last_energies[-1] - max_en_ind = last_energies.argmax() - max_en = last_energies[max_en_ind] - print( "Barrier heights using actual energies (not splined) from " - f"cycle {energies.shape[0]-1}.") - print(f"\tHighest energy image (HEI) at index {max_en_ind} (0-based)") - - first_barr = max_en - first_image_en - print(f"\tBarrier between first image and HEI: {first_barr:.1f} kJ mol⁻¹") - last_barr = max_en - last_image_en - print(f"\tBarrier between last image and HEI: {last_barr:.1f} kJ mol⁻¹") - - # Also do an animation - plotter = Plotter(coords, energies, "ΔE / au", interval=250, save=False) - # This calls plt.show() - plotter.animate() - - -def plot_aneb(): - keys = ("energy", "cart_coords") - (energies, coords), num_cycles, num_images = load_results(keys) - - # Use coordinates of the first image in the first cycle as - # anchor for all following cycles. - first_coords = coords[0][0] - - coord_diffs = list() - min_ = 0 - max_ = max(energies[0]) - for en, c in zip(energies, coords): - cd = np.linalg.norm(c - first_coords, axis=1) - min_ = min(min_, min(en)) - max_ = max(max_, max(en)) - coord_diffs.append(cd) - - energies_ = list() - au2kJmol = 2625.499638 - for en in energies: - en = np.array(en) - en -= min_ - en *= au2kJmol - energies_.append(en) - - fig, ax = plt.subplots() - # Initial energies - lines = ax.plot(coord_diffs[0], energies_[0], "o-") - y_max = (max_ - min_) * au2kJmol - ax.set_ylim(0, y_max) - - ax.set_xlabel("Coordinate differences / Bohr") - ax.set_ylabel("$\Delta$J / kJ $\cdot$ mol$^{-1}$") - - def update_func(i): - fig.suptitle("Cycle {}".format(i)) - lines[0].set_xdata(coord_diffs[i]) - lines[0].set_ydata(energies_[i]) - - def animate(): - animation = FuncAnimation( - fig, - update_func, - frames=num_cycles, - interval=250, - ) - return animation - anim = animate() - plt.show() - - -def load_results(keys): - if isinstance(keys, str): - keys = (keys, ) - image_results_fn = "image_results.yaml" - print(f"Reading {image_results_fn}") - with open(image_results_fn) as handle: - all_results = yaml.load(handle.read(), Loader=yaml.Loader) - num_cycles = len(all_results) - - results_list = list() - for key in keys: - tmp_list = list() - for res_per_cycle in all_results: - try: - tmp_list.append([res[key] for res in res_per_cycle]) - except KeyError: - print(f"Key '{key}' not present in {image_results_fn}. Exiting.") - sys.exit() - results_list.append(np.array(tmp_list)) - # The length of the second axis correpsonds to the number of images - # Determine the number of images. If we have the same number of images - # set num_images to this number. Otherwise return a list containing - # the number of images. - num_images = np.array([len(cycle) for cycle in results_list[0]]) - if all(num_images[0] == num_images): - num_images = num_images[0] - print(f"Found path with {num_images} images.") - # Flatten the first axis when we got only a single key - if len(results_list) == 1: - results_list = results_list[0] - print(f"Loaded {num_cycles} cycle(s).") - return results_list, num_cycles, num_images - - -def plot_cosgrad(): - keys = ("energy", "forces", "coords") - (energies, forces, coords), num_cycles, num_images = load_results(keys) - atom_num = coords[0][0].size // 3 - dummy_atoms =["H"] * atom_num - - all_nebs = list() - all_perp_forces = list() - for i, per_cycle in enumerate(zip(energies, forces, coords), 1): - ens, frcs, crds = per_cycle - images = [Geometry(dummy_atoms, per_image) for per_image in crds] - for image, en, frc in zip(images, ens, frcs): - image._energy = en - image._forces = frc - - neb = NEB(images) - all_nebs.append(neb) - pf = neb.perpendicular_forces.reshape(num_images, -1) - all_perp_forces.append(pf) - - # Calculate norms of true force - # Shape (cycles, images, coords) - force_norms = np.linalg.norm(forces, axis=2) - all_max_forces = list() - all_rms_forces = list() - rms = lambda arr: np.sqrt(np.mean(np.square(arr))) - for pf in all_perp_forces: - max_forces = pf.max(axis=1) - all_max_forces.append(max_forces) - rms_forces = np.apply_along_axis(rms, 1, pf) - all_rms_forces.append(rms_forces) - all_max_forces = np.array(all_max_forces) - all_rms_forces = np.array(all_rms_forces) - - fig, (ax0, ax1, ax2) = plt.subplots(sharex=True, nrows=3) - def plot(ax, data, title): - colors = matplotlib.cm.Greys(np.linspace(0, 1, num=data.shape[0])) - for row, color in zip(data, colors): - ax.plot(row, "o-", color=color) - ax.set_yscale('log') - if title: - ax.set_title(title) - plot(ax0, all_max_forces, "max(perpendicular gradient)") - plot(ax1, all_rms_forces, "rms(perpendicular gradient)") - plot(ax2, force_norms, "norm(true gradient)") - ax2.set_xlabel("Images") - - plt.tight_layout() - plt.show() - - -def plot_multistate_pes(keys): - (pes_ens, coords), num_cycles, num_images = load_results(keys) - pes_ens -= pes_ens.min(axis=(2, 1), keepdims=True) - pes_ens *= 27.211396 - - plotter = Plotter(coords, pes_ens, "ΔE / eV") - plotter.animate() - - -def plot_params(inds): - def get_bond_length(coords_slice): - return np.linalg.norm(coords_slice[0]-coords_slice[1]) * BOHR2ANG * 100 - - def get_angle(coords_slice): - vec1 = coords_slice[0] - coords_slice[1] - vec2 = coords_slice[2] - coords_slice[1] - vec1n = np.linalg.norm(vec1) - vec2n = np.linalg.norm(vec2) - dotp = np.dot(vec1, vec2) - radians = np.arccos(dotp / (vec1n * vec2n)) - return radians * 180 / np.pi - - def get_dihedral(coords_slice): - raise Exception("Not implemented yet!") - - type_dict = { - 2: ("bond length / pm", get_bond_length), - 3: ("angle / °", get_angle), - 4: ("dihedral / °", get_dihedral) - } - inds_list = [[int(i) for i in i_.split()] for i_ in inds.split(",")] - ylabels, funcs = zip(*[type_dict[len(inds)] for inds in inds_list]) - assert all([len(inds_list[i]) == len(inds_list[i+1]) - for i in range(len(inds_list)-1)]), "Can only display " \ - "multiple coordinates of the same type (bond, angle or " \ - "dihedral." - # Just use the first label because they all have to be the same - ylabel = ylabels[0] - - key = "coords" - # only allow same type of coordinate if multiple coordinates are given? - coords, num_cycles, num_images = load_results(key) - - # Coordinates for all images for all cycles - ac = list() - for i, per_cycle in enumerate(coords): - # Coordinates for all images per cycle - pc = list() - for j, per_image in enumerate(per_cycle): - # Coordinates per ind for all images - pi = list() - for inds, func in zip(inds_list, funcs): - coords_slice = per_image.reshape(-1, 3)[inds] - param = func(coords_slice) - pi.append(param) - pc.append(pi) - ac.append(pc) - - ac_arr = np.array(ac) - - # Construct legend list - legend = ["-".join([str(i) for i in inds]) for inds in inds_list] - plotter = Plotter(coords, ac_arr, ylabel, legend=legend) - plotter.animate() - - #df = pd.DataFrame(ac_arr) - #cmap = plt.get_cmap("Greys") - #ax = df.plot( - # title=f"Params {inds}", - # colormap=cmap, - # legend=False, - # marker="o", - # xticks=range(num_images), - # xlim=(0, num_images-1), - #) - #ax.set_xlabel("Image") - #ax.set_ylabel(ylabel) - #plt.tight_layout() - plt.show() - - -def plot_all_energies(h5): - with h5py.File(h5) as handle: - energies = handle["all_energies"][:] - roots = handle["roots"][:] - flips = handle["root_flips"][:] - print(f"Found a total of {len(roots)} steps.") - print(f"{flips} root flips occured.") - - energies -= energies.min() - energies *= AU2EV - - # Don't plot steps where flips occured - # energies = np.concatenate((energies[0][None,:], energies[1:,:][~flips]), axis=0) - energies_ = list() - roots_ = list() - steps = list() - for i, root_flip in enumerate(flips[:-1]): - if root_flip: - print(f"Root flip occured between {i} and {i+1}.") - continue - print(f"Using step {i}") - energies_.append(energies[i]) - roots_.append(roots[i]) - steps.append(i) - # Don't append last step if a root flip occured there. - if not flips[-1]: - energies_.append(energies[-1]) - roots_.append(roots[-1]) - steps.append(i+1) - else: - print("Root flip occured in the last step. Not showing the last step.") - - energies = np.array(energies_) - roots = np.array(roots_) - - fig, ax = plt.subplots() - for i, state in enumerate(energies.T): - ax.plot(steps, state, "o-", label=f"State {i:03d}") - ax.legend(loc="lower center", ncol=3) - ax.set_xlabel("Step") - ax.set_ylabel("$\Delta E / eV$") - root_ens = [s[r] for s, r in zip(energies, roots)] - ax.plot(steps, root_ens, "--k") - plt.show() - - -def plot_bare_energies(h5): - with h5py.File(h5) as handle: - energies = handle["all_energies"][:] - print(f"Found a total of {len(energies)} steps.") - - energies -= energies.min() - energies *= AU2EV - steps = np.arange(len(energies)) - - fig, ax = plt.subplots() - for i, state in enumerate(energies.T): - ax.plot(steps, state, "o-", label=f"State {i:03d}") - ax.legend(loc="lower center", ncol=3) - ax.set_xlabel("Step") - ax.set_ylabel("$\Delta E / eV$") - plt.show() - - -def plot_overlaps(h5, thresh=.1): - with h5py.File(h5) as handle: - overlaps = handle["overlap_matrices"][:] - ovlp_type = handle["ovlp_type"][()].decode() - ovlp_with = handle["ovlp_with"][()].decode() - roots = handle["roots"][:] - calculated_roots = handle["calculated_roots"][:] - ref_cycles = handle["ref_cycles"][:] - ref_roots = handle["ref_roots"][:] - try: - cdd_img_fns = handle["cdd_imgs"][:] - except KeyError: - print(f"Couldn't find image data in '{h5}'.") - try: - with open(CDD_PNG_FNS) as handle: - cdd_img_fns = handle.read().split() - print(f"Found image data in '{CDD_PNG_FNS}'") - except FileNotFoundError: - cdd_img_fns = None - cdd_imgs = None - if cdd_img_fns is not None: - try: - cdd_imgs = [mpimg.imread(fn) for fn in cdd_img_fns] - except FileNotFoundError: - png_paths = [Path(fn.decode()).name for fn in cdd_img_fns] - cdd_imgs = [mpimg.imread(fn) for fn in png_paths] - - overlaps[np.abs(overlaps) < thresh] = np.nan - print(f"Found {len(overlaps)} overlap matrices.") - print(f"Roots: {roots}") - print(f"Reference cycles: {ref_cycles}") - print(f"Reference roots: {ref_roots}") - - print("Key-bindings:") - print("i: switch between current and first cycle.") - print("e: switch between current and last cycle.") - - fig, ax = plt.subplots() - - n_states = overlaps[0].shape[0] - - def draw(i): - fig.clf() - if cdd_imgs is not None: - ax = fig.add_subplot(121) - ax1 = fig.add_subplot(122) - else: - ax = fig.add_subplot(111) - ax1 = None - o = np.abs(overlaps[i]) - im = ax.imshow(o, vmin=0, vmax=1) - # fig.colorbar(im) - ax.grid(color="#CCCCCC", linestyle='--', linewidth=1) - ax.set_xticks(np.arange(n_states, dtype=np.int)) - ax.set_yticks(np.arange(n_states, dtype=np.int)) - ax.set_xlabel("new states") - ax.set_ylabel("reference states") - for (l,k), value in np.ndenumerate(o): - if np.isnan(value): - continue - value_str = f"{abs(value):.2f}" - ax.text(k, l, value_str, ha='center', va='center') - j, k = ref_cycles[i], i+1 - ref_root = ref_roots[i] - ref_ind = ref_root - 1 - old_root = calculated_roots[i+1] - new_root = roots[i+1] - ref_overlaps = o[ref_ind] - if ovlp_type == "wf": - ref_ind += 1 - argmax = np.nanargmax(ref_overlaps) - xy = (argmax-0.5, ref_ind-0.5) - highlight = Rectangle(xy, 1, 1, - fill=False, color="red", lw="4") - ax.add_artist(highlight) - if ax1: - ax1.imshow(cdd_imgs[i]) - fig.suptitle(f"overlap {i:03d}\n" - f"{ovlp_type} overlap between {j:03d} and {k:03d}\n" - f"old root: {old_root}, new root: {new_root}") - fig.canvas.draw() - draw(0) - - i = 0 - i_backup = i - i_last = len(overlaps)-1 - def press(event): - nonlocal i - nonlocal i_backup - if event.key == "left": - i = max(0, i-1) - elif event.key == "right": - i = min(i_last, i+1) - # Switch between current and first cycle - elif event.key == "i": - if i == 0: - # Restore previous cycle - i = i_backup - else: - # Save current i and jump to the first cycle/image - i_backup = i - i = 0 - # Switch between current and last cycle - elif event.key == "e": - if i == i_last: - # Restore previous cycle - i = i_backup - else: - # Save current i and jump to the first cycle/image - i_backup = i - i = i_last - else: - return - draw(i) - fig.canvas.mpl_connect("key_press_event", press) - plt.show() - - -def render_cdds(h5): - with h5py.File(h5) as handle: - cdd_cubes = handle["cdd_cubes"][:].astype(str) - orient = handle["orient"][()].decode() - cdd_cubes = [Path(cub) for cub in cdd_cubes] - print(f"Found {len(cdd_cubes)} CDD cube filenames in {h5}") - # Check if cubes exist - non_existant_cubes = [cub for cub in cdd_cubes if not cub.exists()] - existing_cubes = [str(cub) for cub in set(cdd_cubes) - set(non_existant_cubes)] - if any(non_existant_cubes): - print("Couldn't find cubes:") - print("\n".join(["\t" + str(cub) for cub in non_existant_cubes])) - print("Dropping full path and looking only for cube names.") - cub_names = [cub.name for cub in non_existant_cubes] - _ = [cub for cub in cub_names if Path(cub).exists()] - existing_cubes = existing_cubes + _ - cdd_cubes = existing_cubes - - # Create list of all final PNG filenames - png_fns = [Path(cube).with_suffix(".png") for cube in cdd_cubes] - # Check which cubes are already rendered - png_stems = [png.stem for png in png_fns - if png.exists()] - print(f"{len(png_stems)} cubes seem already rendered.") - - # Only render cubes that are not yet rendered - cdd_cubes = [cube for cube in cdd_cubes - if Path(cube).stem not in png_stems] - print(f"Rendering {len(cdd_cubes)} CDD cubes.") - - for i, cube in enumerate(cdd_cubes): - print(f"Rendering cube {i+1:03d}/{len(cdd_cubes):03d}") - _ = render_cdd_cube(cube, orient=orient) - joined = "\n".join([str(fn) for fn in png_fns]) - with open(CDD_PNG_FNS, "w") as handle: - handle.write(joined) - print("Rendered PNGs:") - print(joined) - print(f"Wrote list of rendered PNGs to '{CDD_PNG_FNS}'") - - -def plot_afir(): - with open("image_results.yaml") as handle: - res = yaml.load(handle.read(), Loader=yaml.loader.Loader) - - afir_ens = [_["energy"] for _ in res] - true_ens = [_["true_energy"] for _ in res] - afir_ens = np.array(afir_ens) * AU2KJPERMOL - afir_ens -= afir_ens.min() - true_ens = np.array(true_ens) * AU2KJPERMOL - true_ens -= true_ens.min() - - afir_forces = np.linalg.norm([_["forces"] for _ in res], axis=1) - true_forces = np.linalg.norm([_["true_forces"] for _ in res], axis=1) - afir_forces = np.array(afir_forces) - true_forces = np.array(true_forces) - - - fig, (en_ax, forces_ax) = plt.subplots(nrows=2, sharex=True) - - style1 = "r--" - style2 = "g--" - style3 = "bo-" - - l1 = en_ax.plot(afir_ens, style1, label="AFIR") - l2 = en_ax.plot(true_ens, style2, label="True") - en_ax2 = en_ax.twinx() - l3 = en_ax2.plot(true_ens+afir_ens, style3, label="Sum") - en_ax2.tick_params(axis="y", labelcolor="blue") - - lines = l1 + l2 + l3 - labels = [l.get_label() for l in lines] - en_ax.legend(lines, labels, loc=0) - - en_ax.set_title("Energies") - en_ax.set_ylabel("$\Delta$E kJ / mol") - - forces_ax.set_title("||Forces||") - l1 = forces_ax.plot(afir_forces, style1, label="AFIR") - l2 = forces_ax.plot(true_forces, style2, label="True") - - forces_ax2 = forces_ax.twinx() - l3 = forces_ax2.plot(true_forces + afir_forces, style3, label="Sum") - forces_ax2.tick_params(axis="y", labelcolor="blue") - - lines = l1 + l2 + l3 - labels = [l.get_label() for l in lines] - forces_ax.legend(lines, labels, loc=0) - - peak_inds, _ = peakdetect(true_ens, lookahead=2) - print(f"Peaks: {peak_inds}") - try: - peak_xs, peak_ys = zip(*peak_inds) - highest = np.argmax(peak_ys) - - en_ax.scatter(peak_xs, peak_ys, s=100, marker="X", c="k", zorder=10) - en_ax.scatter(peak_xs[highest], peak_ys[highest], - s=150, marker="X", c="k", zorder=10) - en_ax.axvline(peak_xs[highest], c="k", ls="--") - forces_ax.axvline(peak_xs[highest], c="k", ls="--") - except ValueError as err: - print("Peak-detection failed!") - - # fig.legend(loc="upper right") - plt.tight_layout() - plt.show() - - -def parse_args(args): - parser = argparse.ArgumentParser() - parser.add_argument("--first", type=int, - help="Only consider the first [first] cycles.") - parser.add_argument("--last", type=int, - help="Only consider the last [last] cycles.") - parser.add_argument("--h5", default="overlap_data.h5") - parser.add_argument("--orient", default="") - - group = parser.add_mutually_exclusive_group(required=True) - group.add_argument("--saras", action="store_true", - help="Plot OpenMolcas state average potential energy " - "surfaces over the course of the NEB.") - group.add_argument("--tddft", action="store_true", - help="Plot ORCA TDDFT potential energy surfaces " - "over the course of the NEB.") - group.add_argument("--params", - help="Follow internal coordinates over the course of " - "the NEB. All atom indices have to be 0-based. " - "Use two indices for a bond, three indices for " - "an angle and four indices for a dihedral. " - "The indices for different coordinates have to " - "be separated by ','.") - group.add_argument("--cosgrad", "--cg", action="store_true", - help="Plot image gradients along the path.") - group.add_argument("--energies", "-e", action="store_true", - help="Plot energies.") - group.add_argument("--aneb", action="store_true", - help="Plot Adaptive NEB.") - group.add_argument("--all_energies", "-a", action="store_true", - help="Plot ground and excited state energies from 'overlap_data.h5'." - ) - group.add_argument("--bare_energies", "-b", action="store_true", - help="Plot ground and excited state energies from 'overlap_data.h5'." - ) - group.add_argument("--afir", action="store_true", - help="Plot AFIR and true -energies and -forces from an AFIR calculation." - ) - group.add_argument("--opt", action="store_true", - help="Plot optimization progress." - ) - group.add_argument("--irc", action="store_true", - help="Plot IRC progress." - ) - group.add_argument("--overlaps", "-o", action="store_true") - group.add_argument("--render_cdds", action="store_true") - - return parser.parse_args(args) - - -def plot_opt(h5_fn="optimization.h5", group_name="opt"): - with h5py.File("optimization.h5", "r") as handle: - group = handle[group_name] - cur_cycle = group["cur_cycle"][()] - ens = group["energies"][:cur_cycle] - is_cos = group["is_cos"][()] - max_forces = group["max_forces"][:cur_cycle] - rms_forces = group["rms_forces"][:cur_cycle] - - ens -= ens.min() - ens *= AU2KJPERMOL - if is_cos: - print("COS optimization detected. Plotting total energy of all images " - "in every cycle. Results from optimizing growing COS methods can " - "be plotted but the plots are not really useful as the varying " - "number of images is not considered.") - ens = ens.sum(axis=1) - - ax_kwargs = { - "marker": "o", - } - - fig, (ax0, ax1, ax2) = plt.subplots(nrows=3, sharex=True) - - ax0.plot(ens, **ax_kwargs) - ax0.set_ylabel("$\Delta E$ / kJ mol⁻¹") - - ax1.plot(max_forces, **ax_kwargs) - ax1.set_title("max(forces)") - ax1.set_ylabel("$E_h$ Bohr⁻¹ (rad)⁻¹") - - ax2.plot(rms_forces, **ax_kwargs) - ax2.set_title("rms(forces)") - ax2.set_xlabel("Step") - ax2.set_ylabel("$E_h$ Bohr⁻¹ (rad)⁻¹") - - fig.suptitle(str(h5_fn) + "/" + group_name) - plt.show() - - -def plot_irc(): - cwd = Path(".") - h5s = cwd.glob("*irc_data.h5") - for h5 in h5s: - type_ = h5.name.split("_")[0] - title = f"{type_.capitalize()} IRC data" - _ = plot_irc_h5(h5, title) - plt.show() - - -def plot_irc_h5(h5, title=None): - print(f"Reading IRC data {h5}") - with h5py.File(h5, "r") as handle: - mw_coords = handle["mw_coords"][:] - energies = handle["energies"][:] - gradients = handle["gradients"][:] - rms_grad_thresh = handle["rms_grad_thresh"][()] - try: - ts_index = handle["ts_index"][()] - except KeyError: - ts_index = None - - energies -= energies[0] - energies *= AU2KJPERMOL - - cds = np.linalg.norm(mw_coords - mw_coords[0], axis=1) - rms_grads = np.sqrt(np.mean(gradients**2, axis=1)) - max_grads = np.abs(gradients).max(axis=1) - - fig, (ax0, ax1, ax2) = plt.subplots(nrows=3, sharex=True) - - plt_kwargs = { - "linestyle": "-", - "marker": "o", - } - - ax0.plot(cds, energies, **plt_kwargs) - ax0.set_title("energy change") - ax0.set_ylabel("kJ mol⁻¹") - - ax1.plot(cds, rms_grads, **plt_kwargs) - ax1.axhline(rms_grad_thresh, linestyle="--", color="k") - ax1.set_title("rms(gradient)") - ax1.set_ylabel("Hartree / bohr") - - ax2.plot(cds, max_grads, **plt_kwargs) - ax2.set_title("max(gradient)") - ax2.set_xlabel("IRC / amu$^{\\frac{1}{2}}$ bohr") - ax2.set_ylabel("Hartree / bohr") - - if ts_index: - x = cds[ts_index] - for ax, arr in ((ax0, energies), (ax1, rms_grads), (ax2, max_grads)): - xy = (x, arr[ts_index]) - ax.annotate("TS", xy, fontsize=12, fontweight="bold") - - if title: - fig.suptitle(title) - else: - fig.tight_layout() - - return fig, (ax0, ax1, ax2) - - -def run(): - args = parse_args(sys.argv[1:]) - - h5 = args.h5 - - if args.energies: - plot_energies() - elif args.saras: - keys = ("sa_energies", "coords") - plot_multistate_pes(keys) - elif args.tddft: - keys = ("tddft_energies", "coords") - plot_multistate_pes(keys) - elif args.params: - plot_params(args.params) - elif args.cosgrad: - plot_cosgrad() - elif args.aneb: - plot_aneb() - elif args.all_energies: - plot_all_energies(h5=h5) - elif args.overlaps: - plot_overlaps(h5=h5) - elif args.render_cdds: - render_cdds(h5=h5) - elif args.bare_energies: - plot_bare_energies(h5=h5) - elif args.afir: - plot_afir() - elif args.opt: - plot_opt() - elif args.irc: - plot_irc() - - -if __name__ == "__main__": - run() diff --git a/deprecated/plotters/RFOPlotter.py b/deprecated/plotters/RFOPlotter.py deleted file mode 100644 index 91f72b5352..0000000000 --- a/deprecated/plotters/RFOPlotter.py +++ /dev/null @@ -1,172 +0,0 @@ -import itertools - -import matplotlib.pyplot as plt -import matplotlib.animation as animation -from matplotlib.patches import Circle -import numpy as np - -class RFOPlotter(): - def __init__(self, calc, opt, figsize=(8, 6), - save=False, title=True): - self.opt = opt - self.calc = calc - self.figsize = figsize - self.save = save - self.title = title - - self.xlim = calc.xlim - self.ylim = calc.ylim - self.coords = np.array(self.opt.coords)[:,:2] - self.rfo_steps2 = np.array(opt.rfo_steps)[:,:2] - self.rfo_steps = self.rfo_steps2 + self.coords - - self.fig, (self.ax, self.ax2) = plt.subplots(ncols=2, - figsize=figsize) - self.ax.set_title("True potential") - self.ax2.set_title("Local quadratic approximation") - self.pause = True - self.fig.canvas.mpl_connect('key_press_event', self.on_keypress) - self.get_frame = itertools.cycle(range(self.opt.cur_cycle)) - - def plot(self): - # Draw potential as contour lines - self.xs = np.linspace(*self.xlim, 100) - self.ys = np.linspace(*self.ylim, 100) - X, Y = np.meshgrid(self.xs, self.ys) - Z = np.full_like(X, 0) - fake_atoms = ("X", ) - pot_coords = np.stack((X, Y, Z)) - pot = self.calc.get_energy(fake_atoms, pot_coords)["energy"] - levels = np.linspace(pot.min(), pot.max(), 25) - contours = self.ax.contour(X, Y, pot, levels) - - # Calculate the LQA potentials for every cycle (frame) - def quadratic_approx(cycle, step): - E0 = self.opt.energies[cycle] - g = -self.opt.forces[cycle] - H = self.opt.hessians[cycle] - return E0 + np.inner(g, step) + 0.5*step.dot(H.dot(step)) - - self.lqa_pots = list() - for cycle in range(self.opt.cur_cycle): - cycle_pot = list() - for x in self.xs: - for y in self.ys: - step = np.array((x, y, 0)) - E = quadratic_approx(cycle, step) - cycle_pot.append(E) - cycle_pot = np.array(cycle_pot).reshape(-1, self.ys.size) - self.lqa_pots.append(cycle_pot) - self.lqa_pots = np.array(self.lqa_pots) - # Draw LQA potential as contour lines - self.lqa_contour = self.ax2.contour(self.xs, self.ys, self.lqa_pots[0]) - - # Draw the actual geometries - self.coord_lines, = self.ax.plot(*self.coords[0], "X", - label="Geometry") - # Draw the pure RFO steps - rfo_kwargs = { - "marker": "x", - "c": "r", - "label": "Pure RFO step", - } - self.rfo_lines, = self.ax.plot(*self.rfo_steps[0], **rfo_kwargs) - self.rfo_lines2, = self.ax2.plot(*self.rfo_steps2[0], **rfo_kwargs) - - # Draw the actual steps taken - actual_kwargs = { - "marker": "x", - "c": "k", - "label": "Actual step", - } - self.steps = np.array(self.opt.steps)[:,:2] - act_steps1 = self.steps[0] + self.coords[0] - self.actual_step_lines, = self.ax.plot(*act_steps1, **actual_kwargs) - self.actual_step_lines2, = self.ax2.plot(*self.steps[0], **actual_kwargs) - - - circle_kwargs = { - "fill": False, - "color": "k", - } - #self.trust_region = Circle(self.coords[0], radius=self.opt.trust_radii[0], - # **circle_kwargs) - #self.ax.add_patch(self.trust_region) - self.trust_region2 = Circle((0, 0), radius=self.opt.trust_radii[0], - **circle_kwargs) - self.ax2.add_patch(self.trust_region2) - self.ax2.plot(0, 0, "X") - - self.ax.legend() - - def update_plot(self, frame): - if self.title: - self.fig.suptitle(f"Cycle {frame}") - - # Update the geometry in ax - coords_x = self.coords[frame, 0] - coords_y = self.coords[frame, 1] - self.coord_lines.set_xdata(coords_x) - self.coord_lines.set_ydata(coords_y) - - # Update LQA contours in ax2 - self.lqa_contour.set_array(self.lqa_pots[frame]) - for tp in self.lqa_contour.collections: - tp.remove() - cycle_x, cycle_y = self.coords[frame] - self.lqa_contour = self.ax2.contour(self.xs, self.ys, self.lqa_pots[frame]) - #self.trust_region.center = (cycle_x, cycle_y) - #self.trust_region.set_radius(self.opt.trust_radii[frame]) - - # Update trust region in ax2 - self.trust_region2.set_radius(self.opt.trust_radii[frame]) - - # Update the actual steps taken - actual_step_x = self.steps[frame, 0] - actual_step_y = self.steps[frame, 1] - self.actual_step_lines.set_xdata(actual_step_x+cycle_x) - self.actual_step_lines.set_ydata(actual_step_y+cycle_y) - self.actual_step_lines2.set_xdata(actual_step_x) - self.actual_step_lines2.set_ydata(actual_step_y) - - # Update the (potentially bigger than allowed) RFO steps - rfo_x = self.rfo_steps[frame, 0] - rfo_y = self.rfo_steps[frame, 1] - self.rfo_lines.set_xdata(rfo_x) - self.rfo_lines.set_ydata(rfo_y) - rfo2_x = self.rfo_steps2[frame, 0] - rfo2_y = self.rfo_steps2[frame, 1] - self.rfo_lines2.set_xdata(rfo2_x) - self.rfo_lines2.set_ydata(rfo2_y) - plt.tight_layout() - - if self.save: - frame_fn = f"step{frame}.png" - self.fig.savefig(frame_fn) - # if not os.path.exists(frame_fn): - # self.fig.savefig(frame_fn) - - # def animate(self): - # self.interval = 2000 - # frames = range(self.opt.cur_cycle) - # self.animation = animation.FuncAnimation(self.fig, - # self.update_plot, - # frames=frames, - # interval=self.interval) - - def on_keypress(self, event): - """Advance the plot by one cycle (frame).""" - if event.key == " ": - frame = next(self.get_frame) - self.update_plot(frame) - plt.draw() - - #def on_keypress(self, event): - # """Pause on SPACE press.""" - # #https://stackoverflow.com/questions/41557578 - # if event.key == " ": - # if self.pause: - # self.animation.event_source.stop() - # else: - # self.animation.event_source.start() - # self.pause = not self.pause diff --git a/deprecated/scripts/overlaps.py b/deprecated/scripts/overlaps.py deleted file mode 100755 index 5f8992dda8..0000000000 --- a/deprecated/scripts/overlaps.py +++ /dev/null @@ -1,126 +0,0 @@ -#!/usr/bin/env python3 - -"""Example YAML input: - -geom: - fn: lib:h2o2_hf_321g_opt.xyz -calc1: - type: orca5 - keywords: hf sto-3g - blocks: "%tddft nroots 2 iroot 1 end" - pal: 2 -calc2: - type: orca5 - keywords: hf sto-3g - blocks: "%tddft nroots 2 iroot 1 end" - pal: 2 -# Either wf|tden -ovlp_type: wf - -""" - -import argparse -from pprint import pprint -import sys -import time - -import numpy as np -import yaml - -from pysisyphus.calculators import ORCA, ORCA5, Gaussian16 -from pysisyphus.helpers import geom_loader -from pysisyphus.init_logging import init_logging - - -init_logging() -np.set_printoptions(suppress=True, precision=6) - - -def parse_args(args): - - parser = argparse.ArgumentParser() - - parser.add_argument("yaml") - parser.add_argument("--ovlp-fn", dest="ovlp_fn", default="ovlp_mat.dat") - parser.add_argument("--skip-calcs", dest="do_calc", action="store_false") - parser.add_argument("--h5-fns", dest="h5_fns", nargs=2, default=None) - parser.add_argument("--conf-thresh", dest="conf_thresh", default=0.001, type=float) - - return parser.parse_args(args) - - -def run(): - args = parse_args(sys.argv[1:]) - - with open(args.yaml) as handle: - run_dict = yaml.load(handle.read(), Loader=yaml.SafeLoader) - pprint(run_dict) - print() - - geom = geom_loader(run_dict["geom"]["fn"]) - - CALCS = {"orca": ORCA, "gaussian16": Gaussian16, "orca5": ORCA5} - - ovlp_type = run_dict["ovlp_type"] - - def get_calc(key): - calc_kwargs = run_dict[key] - calc_kwargs["ovlp_type"] = ovlp_type - calc_key = calc_kwargs.pop("type") - dump_fn = f"overlap_data_{key}.h5" - calc = CALCS[calc_key](**calc_kwargs, base_name=key, dump_fn=dump_fn) - assert calc.root, "No 'root' set on calculator. Please specify an initial root." - return calc - - calc1 = get_calc("calc1") - calc2 = get_calc("calc2") - - calc_args = (geom.atoms, geom.coords) - - def calc_es(calc): - print(f"Calculating ES for {calc} ... ", end="") - start = time.time() - calc.get_energy(*calc_args) - dur = time.time() - start - print(f"finished in {dur:.1f} s.") - calc.store_overlap_data(*calc_args) - calc.dump_overlap_data() - - if args.do_calc: - calc_es(calc1) - calc_es(calc2) - else: - try: - h5_fn1, h5_fn2 = args.h5_fns - except TypeError: - h5_fn1 = calc1.dump_fn - h5_fn2 = calc2.dump_fn - print(f"Taking overlap_data from '{h5_fn1}' and '{h5_fn2}'.") - calc1 = calc1.from_overlap_data(h5_fn1, set_wfow=True) - calc2 = calc2.from_overlap_data(h5_fn2) - - conf_thresh = args.conf_thresh - calc1.conf_thresh = conf_thresh - if ovlp_type == "wf": - calc1.wfow.conf_thresh = conf_thresh - - ao_ovlp = calc1.get_sao_from_mo_coeffs(calc1.mo_coeff_list[-1]) - print("Recreate S_AO from MO coeffs at calc1") - ovlp_funcs = { - "tden": "tden_overlap_with_calculator", - "wf": "wf_overlap_with_calculator", - } - ovlp_func = ovlp_funcs[ovlp_type] - print(f"Calculating {ovlp_type} overlaps") - ovlp_mat = getattr(calc1, ovlp_func)(calc2, ao_ovlp=ao_ovlp) - if ovlp_type == "wf": - ovlp_mat = ovlp_mat[0] - print("Rows along states of calc1, columns along states of calc2") - print(ovlp_mat) - ovlp_fn = f"{ovlp_type}_{args.ovlp_fn}" - np.savetxt(ovlp_fn, ovlp_mat) - print(f"Dumped overlap matrix to '{ovlp_fn}'.") - - -if __name__ == "__main__": - run() diff --git a/deprecated/server/bottle.py b/deprecated/server/bottle.py deleted file mode 100644 index 90d0bd7c62..0000000000 --- a/deprecated/server/bottle.py +++ /dev/null @@ -1,4418 +0,0 @@ -#!/usr/bin/env python -# -*- coding: utf-8 -*- -""" -Bottle is a fast and simple micro-framework for small web applications. It -offers request dispatching (Routes) with URL parameter support, templates, -a built-in HTTP Server and adapters for many third party WSGI/HTTP-server and -template engines - all in a single file and with no dependencies other than the -Python Standard Library. - -Homepage and documentation: http://bottlepy.org/ - -Copyright (c) 2009-2018, Marcel Hellkamp. -License: MIT (see LICENSE for details) -""" - -import sys - -__author__ = 'Marcel Hellkamp' -__version__ = '0.13-dev' -__license__ = 'MIT' - -############################################################################### -# Command-line interface ###################################################### -############################################################################### -# INFO: Some server adapters need to monkey-patch std-lib modules before they -# are imported. This is why some of the command-line handling is done here, but -# the actual call to _main() is at the end of the file. - - -def _cli_parse(args): # pragma: no coverage - from argparse import ArgumentParser - - parser = ArgumentParser(prog=args[0], usage="%(prog)s [options] package.module:app") - opt = parser.add_argument - opt("--version", action="store_true", help="show version number.") - opt("-b", "--bind", metavar="ADDRESS", help="bind socket to ADDRESS.") - opt("-s", "--server", default='wsgiref', help="use SERVER as backend.") - opt("-p", "--plugin", action="append", help="install additional plugin/s.") - opt("-c", "--conf", action="append", metavar="FILE", - help="load config values from FILE.") - opt("-C", "--param", action="append", metavar="NAME=VALUE", - help="override config values.") - opt("--debug", action="store_true", help="start server in debug mode.") - opt("--reload", action="store_true", help="auto-reload on file changes.") - opt('app', help='WSGI app entry point.', nargs='?') - - cli_args = parser.parse_args(args[1:]) - - return cli_args, parser - - -def _cli_patch(cli_args): # pragma: no coverage - parsed_args, _ = _cli_parse(cli_args) - opts = parsed_args - if opts.server: - if opts.server.startswith('gevent'): - import gevent.monkey - gevent.monkey.patch_all() - elif opts.server.startswith('eventlet'): - import eventlet - eventlet.monkey_patch() - - -if __name__ == '__main__': - _cli_patch(sys.argv) - -############################################################################### -# Imports and Python 2/3 unification ########################################## -############################################################################### - - -import base64, cgi, email.utils, functools, hmac, imp, itertools, mimetypes,\ - os, re, tempfile, threading, time, warnings, weakref, hashlib - -from types import FunctionType -from datetime import date as datedate, datetime, timedelta -from tempfile import TemporaryFile -from traceback import format_exc, print_exc -from unicodedata import normalize - -try: - from ujson import dumps as json_dumps, loads as json_lds -except ImportError: - from json import dumps as json_dumps, loads as json_lds - -# inspect.getargspec was removed in Python 3.6, use -# Signature-based version where we can (Python 3.3+) -try: - from inspect import signature - def getargspec(func): - params = signature(func).parameters - args, varargs, keywords, defaults = [], None, None, [] - for name, param in params.items(): - if param.kind == param.VAR_POSITIONAL: - varargs = name - elif param.kind == param.VAR_KEYWORD: - keywords = name - else: - args.append(name) - if param.default is not param.empty: - defaults.append(param.default) - return (args, varargs, keywords, tuple(defaults) or None) -except ImportError: - try: - from inspect import getfullargspec - def getargspec(func): - spec = getfullargspec(func) - kwargs = makelist(spec[0]) + makelist(spec.kwonlyargs) - return kwargs, spec[1], spec[2], spec[3] - except ImportError: - from inspect import getargspec - -py3k = sys.version_info.major > 2 - - -# Workaround for the "print is a keyword/function" Python 2/3 dilemma -# and a fallback for mod_wsgi (resticts stdout/err attribute access) -try: - _stdout, _stderr = sys.stdout.write, sys.stderr.write -except IOError: - _stdout = lambda x: sys.stdout.write(x) - _stderr = lambda x: sys.stderr.write(x) - -# Lots of stdlib and builtin differences. -if py3k: - import http.client as httplib - import _thread as thread - from urllib.parse import urljoin, SplitResult as UrlSplitResult - from urllib.parse import urlencode, quote as urlquote, unquote as urlunquote - urlunquote = functools.partial(urlunquote, encoding='latin1') - from http.cookies import SimpleCookie, Morsel, CookieError - from collections.abc import MutableMapping as DictMixin - import pickle - from io import BytesIO - import configparser - - basestring = str - unicode = str - json_loads = lambda s: json_lds(touni(s)) - callable = lambda x: hasattr(x, '__call__') - imap = map - - def _raise(*a): - raise a[0](a[1]).with_traceback(a[2]) -else: # 2.x - import httplib - import thread - from urlparse import urljoin, SplitResult as UrlSplitResult - from urllib import urlencode, quote as urlquote, unquote as urlunquote - from Cookie import SimpleCookie, Morsel, CookieError - from itertools import imap - import cPickle as pickle - from StringIO import StringIO as BytesIO - import ConfigParser as configparser - from collections import MutableMapping as DictMixin - unicode = unicode - json_loads = json_lds - exec(compile('def _raise(*a): raise a[0], a[1], a[2]', '', 'exec')) - -# Some helpers for string/byte handling -def tob(s, enc='utf8'): - if isinstance(s, unicode): - return s.encode(enc) - return b'' if s is None else bytes(s) - - -def touni(s, enc='utf8', err='strict'): - if isinstance(s, bytes): - return s.decode(enc, err) - return unicode("" if s is None else s) - - -tonat = touni if py3k else tob - - - -# A bug in functools causes it to break if the wrapper is an instance method -def update_wrapper(wrapper, wrapped, *a, **ka): - try: - functools.update_wrapper(wrapper, wrapped, *a, **ka) - except AttributeError: - pass - -# These helpers are used at module level and need to be defined first. -# And yes, I know PEP-8, but sometimes a lower-case classname makes more sense. - - -def depr(major, minor, cause, fix): - text = "Warning: Use of deprecated feature or API. (Deprecated in Bottle-%d.%d)\n"\ - "Cause: %s\n"\ - "Fix: %s\n" % (major, minor, cause, fix) - if DEBUG == 'strict': - raise DeprecationWarning(text) - warnings.warn(text, DeprecationWarning, stacklevel=3) - return DeprecationWarning(text) - - -def makelist(data): # This is just too handy - if isinstance(data, (tuple, list, set, dict)): - return list(data) - elif data: - return [data] - else: - return [] - - -class DictProperty(object): - """ Property that maps to a key in a local dict-like attribute. """ - - def __init__(self, attr, key=None, read_only=False): - self.attr, self.key, self.read_only = attr, key, read_only - - def __call__(self, func): - functools.update_wrapper(self, func, updated=[]) - self.getter, self.key = func, self.key or func.__name__ - return self - - def __get__(self, obj, cls): - if obj is None: return self - key, storage = self.key, getattr(obj, self.attr) - if key not in storage: storage[key] = self.getter(obj) - return storage[key] - - def __set__(self, obj, value): - if self.read_only: raise AttributeError("Read-Only property.") - getattr(obj, self.attr)[self.key] = value - - def __delete__(self, obj): - if self.read_only: raise AttributeError("Read-Only property.") - del getattr(obj, self.attr)[self.key] - - -class cached_property(object): - """ A property that is only computed once per instance and then replaces - itself with an ordinary attribute. Deleting the attribute resets the - property. """ - - def __init__(self, func): - update_wrapper(self, func) - self.func = func - - def __get__(self, obj, cls): - if obj is None: return self - value = obj.__dict__[self.func.__name__] = self.func(obj) - return value - - -class lazy_attribute(object): - """ A property that caches itself to the class object. """ - - def __init__(self, func): - functools.update_wrapper(self, func, updated=[]) - self.getter = func - - def __get__(self, obj, cls): - value = self.getter(cls) - setattr(cls, self.__name__, value) - return value - -############################################################################### -# Exceptions and Events ####################################################### -############################################################################### - - -class BottleException(Exception): - """ A base class for exceptions used by bottle. """ - pass - -############################################################################### -# Routing ###################################################################### -############################################################################### - - -class RouteError(BottleException): - """ This is a base class for all routing related exceptions """ - - -class RouteReset(BottleException): - """ If raised by a plugin or request handler, the route is reset and all - plugins are re-applied. """ - - -class RouterUnknownModeError(RouteError): - - pass - - -class RouteSyntaxError(RouteError): - """ The route parser found something not supported by this router. """ - - -class RouteBuildError(RouteError): - """ The route could not be built. """ - - -def _re_flatten(p): - """ Turn all capturing groups in a regular expression pattern into - non-capturing groups. """ - if '(' not in p: - return p - return re.sub(r'(\\*)(\(\?P<[^>]+>|\((?!\?))', lambda m: m.group(0) if - len(m.group(1)) % 2 else m.group(1) + '(?:', p) - - -class Router(object): - """ A Router is an ordered collection of route->target pairs. It is used to - efficiently match WSGI requests against a number of routes and return - the first target that satisfies the request. The target may be anything, - usually a string, ID or callable object. A route consists of a path-rule - and a HTTP method. - - The path-rule is either a static path (e.g. `/contact`) or a dynamic - path that contains wildcards (e.g. `/wiki/`). The wildcard syntax - and details on the matching order are described in docs:`routing`. - """ - - default_pattern = '[^/]+' - default_filter = 're' - - #: The current CPython regexp implementation does not allow more - #: than 99 matching groups per regular expression. - _MAX_GROUPS_PER_PATTERN = 99 - - def __init__(self, strict=False): - self.rules = [] # All rules in order - self._groups = {} # index of regexes to find them in dyna_routes - self.builder = {} # Data structure for the url builder - self.static = {} # Search structure for static routes - self.dyna_routes = {} - self.dyna_regexes = {} # Search structure for dynamic routes - #: If true, static routes are no longer checked first. - self.strict_order = strict - self.filters = { - 're': lambda conf: (_re_flatten(conf or self.default_pattern), - None, None), - 'int': lambda conf: (r'-?\d+', int, lambda x: str(int(x))), - 'float': lambda conf: (r'-?[\d.]+', float, lambda x: str(float(x))), - 'path': lambda conf: (r'.+?', None, None) - } - - def add_filter(self, name, func): - """ Add a filter. The provided function is called with the configuration - string as parameter and must return a (regexp, to_python, to_url) tuple. - The first element is a string, the last two are callables or None. """ - self.filters[name] = func - - rule_syntax = re.compile('(\\\\*)' - '(?:(?::([a-zA-Z_][a-zA-Z_0-9]*)?()(?:#(.*?)#)?)' - '|(?:<([a-zA-Z_][a-zA-Z_0-9]*)?(?::([a-zA-Z_]*)' - '(?::((?:\\\\.|[^\\\\>]+)+)?)?)?>))') - - def _itertokens(self, rule): - offset, prefix = 0, '' - for match in self.rule_syntax.finditer(rule): - prefix += rule[offset:match.start()] - g = match.groups() - if g[2] is not None: - depr(0, 13, "Use of old route syntax.", - "Use instead of :name in routes.") - if len(g[0]) % 2: # Escaped wildcard - prefix += match.group(0)[len(g[0]):] - offset = match.end() - continue - if prefix: - yield prefix, None, None - name, filtr, conf = g[4:7] if g[2] is None else g[1:4] - yield name, filtr or 'default', conf or None - offset, prefix = match.end(), '' - if offset <= len(rule) or prefix: - yield prefix + rule[offset:], None, None - - def add(self, rule, method, target, name=None): - """ Add a new rule or replace the target for an existing rule. """ - anons = 0 # Number of anonymous wildcards found - keys = [] # Names of keys - pattern = '' # Regular expression pattern with named groups - filters = [] # Lists of wildcard input filters - builder = [] # Data structure for the URL builder - is_static = True - - for key, mode, conf in self._itertokens(rule): - if mode: - is_static = False - if mode == 'default': mode = self.default_filter - mask, in_filter, out_filter = self.filters[mode](conf) - if not key: - pattern += '(?:%s)' % mask - key = 'anon%d' % anons - anons += 1 - else: - pattern += '(?P<%s>%s)' % (key, mask) - keys.append(key) - if in_filter: filters.append((key, in_filter)) - builder.append((key, out_filter or str)) - elif key: - pattern += re.escape(key) - builder.append((None, key)) - - self.builder[rule] = builder - if name: self.builder[name] = builder - - if is_static and not self.strict_order: - self.static.setdefault(method, {}) - self.static[method][self.build(rule)] = (target, None) - return - - try: - re_pattern = re.compile('^(%s)$' % pattern) - re_match = re_pattern.match - except re.error as e: - raise RouteSyntaxError("Could not add Route: %s (%s)" % (rule, e)) - - if filters: - - def getargs(path): - url_args = re_match(path).groupdict() - for name, wildcard_filter in filters: - try: - url_args[name] = wildcard_filter(url_args[name]) - except ValueError: - raise HTTPError(400, 'Path has wrong format.') - return url_args - elif re_pattern.groupindex: - - def getargs(path): - return re_match(path).groupdict() - else: - getargs = None - - flatpat = _re_flatten(pattern) - whole_rule = (rule, flatpat, target, getargs) - - if (flatpat, method) in self._groups: - if DEBUG: - msg = 'Route <%s %s> overwrites a previously defined route' - warnings.warn(msg % (method, rule), RuntimeWarning) - self.dyna_routes[method][ - self._groups[flatpat, method]] = whole_rule - else: - self.dyna_routes.setdefault(method, []).append(whole_rule) - self._groups[flatpat, method] = len(self.dyna_routes[method]) - 1 - - self._compile(method) - - def _compile(self, method): - all_rules = self.dyna_routes[method] - comborules = self.dyna_regexes[method] = [] - maxgroups = self._MAX_GROUPS_PER_PATTERN - for x in range(0, len(all_rules), maxgroups): - some = all_rules[x:x + maxgroups] - combined = (flatpat for (_, flatpat, _, _) in some) - combined = '|'.join('(^%s$)' % flatpat for flatpat in combined) - combined = re.compile(combined).match - rules = [(target, getargs) for (_, _, target, getargs) in some] - comborules.append((combined, rules)) - - def build(self, _name, *anons, **query): - """ Build an URL by filling the wildcards in a rule. """ - builder = self.builder.get(_name) - if not builder: - raise RouteBuildError("No route with that name.", _name) - try: - for i, value in enumerate(anons): - query['anon%d' % i] = value - url = ''.join([f(query.pop(n)) if n else f for (n, f) in builder]) - return url if not query else url + '?' + urlencode(query) - except KeyError as E: - raise RouteBuildError('Missing URL argument: %r' % E.args[0]) - - def match(self, environ): - """ Return a (target, url_args) tuple or raise HTTPError(400/404/405). """ - verb = environ['REQUEST_METHOD'].upper() - path = environ['PATH_INFO'] or '/' - - if verb == 'HEAD': - methods = ['PROXY', verb, 'GET', 'ANY'] - else: - methods = ['PROXY', verb, 'ANY'] - - for method in methods: - if method in self.static and path in self.static[method]: - target, getargs = self.static[method][path] - return target, getargs(path) if getargs else {} - elif method in self.dyna_regexes: - for combined, rules in self.dyna_regexes[method]: - match = combined(path) - if match: - target, getargs = rules[match.lastindex - 1] - return target, getargs(path) if getargs else {} - - # No matching route found. Collect alternative methods for 405 response - allowed = set([]) - nocheck = set(methods) - for method in set(self.static) - nocheck: - if path in self.static[method]: - allowed.add(method) - for method in set(self.dyna_regexes) - allowed - nocheck: - for combined, rules in self.dyna_regexes[method]: - match = combined(path) - if match: - allowed.add(method) - if allowed: - allow_header = ",".join(sorted(allowed)) - raise HTTPError(405, "Method not allowed.", Allow=allow_header) - - # No matching route and no alternative method found. We give up - raise HTTPError(404, "Not found: " + repr(path)) - - -class Route(object): - """ This class wraps a route callback along with route specific metadata and - configuration and applies Plugins on demand. It is also responsible for - turing an URL path rule into a regular expression usable by the Router. - """ - - def __init__(self, app, rule, method, callback, - name=None, - plugins=None, - skiplist=None, **config): - #: The application this route is installed to. - self.app = app - #: The path-rule string (e.g. ``/wiki/``). - self.rule = rule - #: The HTTP method as a string (e.g. ``GET``). - self.method = method - #: The original callback with no plugins applied. Useful for introspection. - self.callback = callback - #: The name of the route (if specified) or ``None``. - self.name = name or None - #: A list of route-specific plugins (see :meth:`Bottle.route`). - self.plugins = plugins or [] - #: A list of plugins to not apply to this route (see :meth:`Bottle.route`). - self.skiplist = skiplist or [] - #: Additional keyword arguments passed to the :meth:`Bottle.route` - #: decorator are stored in this dictionary. Used for route-specific - #: plugin configuration and meta-data. - self.config = app.config._make_overlay() - self.config.load_dict(config) - - @cached_property - def call(self): - """ The route callback with all plugins applied. This property is - created on demand and then cached to speed up subsequent requests.""" - return self._make_callback() - - def reset(self): - """ Forget any cached values. The next time :attr:`call` is accessed, - all plugins are re-applied. """ - self.__dict__.pop('call', None) - - def prepare(self): - """ Do all on-demand work immediately (useful for debugging).""" - self.call - - def all_plugins(self): - """ Yield all Plugins affecting this route. """ - unique = set() - for p in reversed(self.app.plugins + self.plugins): - if True in self.skiplist: break - name = getattr(p, 'name', False) - if name and (name in self.skiplist or name in unique): continue - if p in self.skiplist or type(p) in self.skiplist: continue - if name: unique.add(name) - yield p - - def _make_callback(self): - callback = self.callback - for plugin in self.all_plugins(): - try: - if hasattr(plugin, 'apply'): - callback = plugin.apply(callback, self) - else: - callback = plugin(callback) - except RouteReset: # Try again with changed configuration. - return self._make_callback() - if not callback is self.callback: - update_wrapper(callback, self.callback) - return callback - - def get_undecorated_callback(self): - """ Return the callback. If the callback is a decorated function, try to - recover the original function. """ - func = self.callback - func = getattr(func, '__func__' if py3k else 'im_func', func) - closure_attr = '__closure__' if py3k else 'func_closure' - while hasattr(func, closure_attr) and getattr(func, closure_attr): - attributes = getattr(func, closure_attr) - func = attributes[0].cell_contents - - # in case of decorators with multiple arguments - if not isinstance(func, FunctionType): - # pick first FunctionType instance from multiple arguments - func = filter(lambda x: isinstance(x, FunctionType), - map(lambda x: x.cell_contents, attributes)) - func = list(func)[0] # py3 support - return func - - def get_callback_args(self): - """ Return a list of argument names the callback (most likely) accepts - as keyword arguments. If the callback is a decorated function, try - to recover the original function before inspection. """ - return getargspec(self.get_undecorated_callback())[0] - - def get_config(self, key, default=None): - """ Lookup a config field and return its value, first checking the - route.config, then route.app.config.""" - depr(0, 13, "Route.get_config() is deprectated.", - "The Route.config property already includes values from the" - " application config for missing keys. Access it directly.") - return self.config.get(key, default) - - def __repr__(self): - cb = self.get_undecorated_callback() - return '<%s %r %r>' % (self.method, self.rule, cb) - -############################################################################### -# Application Object ########################################################### -############################################################################### - - -class Bottle(object): - """ Each Bottle object represents a single, distinct web application and - consists of routes, callbacks, plugins, resources and configuration. - Instances are callable WSGI applications. - - :param catchall: If true (default), handle all exceptions. Turn off to - let debugging middleware handle exceptions. - """ - - @lazy_attribute - def _global_config(cls): - cfg = ConfigDict() - cfg.meta_set('catchall', 'validate', bool) - return cfg - - def __init__(self, **kwargs): - #: A :class:`ConfigDict` for app specific configuration. - self.config = self._global_config._make_overlay() - self.config._add_change_listener( - functools.partial(self.trigger_hook, 'config')) - - self.config.update({ - "catchall": True - }) - - if kwargs.get('catchall') is False: - depr(0, 13, "Bottle(catchall) keyword argument.", - "The 'catchall' setting is now part of the app " - "configuration. Fix: `app.config['catchall'] = False`") - self.config['catchall'] = False - if kwargs.get('autojson') is False: - depr(0, 13, "Bottle(autojson) keyword argument.", - "The 'autojson' setting is now part of the app " - "configuration. Fix: `app.config['json.enable'] = False`") - self.config['json.disable'] = True - - self._mounts = [] - - #: A :class:`ResourceManager` for application files - self.resources = ResourceManager() - - self.routes = [] # List of installed :class:`Route` instances. - self.router = Router() # Maps requests to :class:`Route` instances. - self.error_handler = {} - - # Core plugins - self.plugins = [] # List of installed plugins. - self.install(JSONPlugin()) - self.install(TemplatePlugin()) - - #: If true, most exceptions are caught and returned as :exc:`HTTPError` - catchall = DictProperty('config', 'catchall') - - __hook_names = 'before_request', 'after_request', 'app_reset', 'config' - __hook_reversed = {'after_request'} - - @cached_property - def _hooks(self): - return dict((name, []) for name in self.__hook_names) - - def add_hook(self, name, func): - """ Attach a callback to a hook. Three hooks are currently implemented: - - before_request - Executed once before each request. The request context is - available, but no routing has happened yet. - after_request - Executed once after each request regardless of its outcome. - app_reset - Called whenever :meth:`Bottle.reset` is called. - """ - if name in self.__hook_reversed: - self._hooks[name].insert(0, func) - else: - self._hooks[name].append(func) - - def remove_hook(self, name, func): - """ Remove a callback from a hook. """ - if name in self._hooks and func in self._hooks[name]: - self._hooks[name].remove(func) - return True - - def trigger_hook(self, __name, *args, **kwargs): - """ Trigger a hook and return a list of results. """ - return [hook(*args, **kwargs) for hook in self._hooks[__name][:]] - - def hook(self, name): - """ Return a decorator that attaches a callback to a hook. See - :meth:`add_hook` for details.""" - - def decorator(func): - self.add_hook(name, func) - return func - - return decorator - - def _mount_wsgi(self, prefix, app, **options): - segments = [p for p in prefix.split('/') if p] - if not segments: - raise ValueError('WSGI applications cannot be mounted to "/".') - path_depth = len(segments) - - def mountpoint_wrapper(): - try: - request.path_shift(path_depth) - rs = HTTPResponse([]) - - def start_response(status, headerlist, exc_info=None): - if exc_info: - _raise(*exc_info) - rs.status = status - for name, value in headerlist: - rs.add_header(name, value) - return rs.body.append - - body = app(request.environ, start_response) - rs.body = itertools.chain(rs.body, body) if rs.body else body - return rs - finally: - request.path_shift(-path_depth) - - options.setdefault('skip', True) - options.setdefault('method', 'PROXY') - options.setdefault('mountpoint', {'prefix': prefix, 'target': app}) - options['callback'] = mountpoint_wrapper - - self.route('/%s/<:re:.*>' % '/'.join(segments), **options) - if not prefix.endswith('/'): - self.route('/' + '/'.join(segments), **options) - - def _mount_app(self, prefix, app, **options): - if app in self._mounts or '_mount.app' in app.config: - depr(0, 13, "Application mounted multiple times. Falling back to WSGI mount.", - "Clone application before mounting to a different location.") - return self._mount_wsgi(prefix, app, **options) - - if options: - depr(0, 13, "Unsupported mount options. Falling back to WSGI mount.", - "Do not specify any route options when mounting bottle application.") - return self._mount_wsgi(prefix, app, **options) - - if not prefix.endswith("/"): - depr(0, 13, "Prefix must end in '/'. Falling back to WSGI mount.", - "Consider adding an explicit redirect from '/prefix' to '/prefix/' in the parent application.") - return self._mount_wsgi(prefix, app, **options) - - self._mounts.append(app) - app.config['_mount.prefix'] = prefix - app.config['_mount.app'] = self - for route in app.routes: - route.rule = prefix + route.rule.lstrip('/') - self.add_route(route) - - def mount(self, prefix, app, **options): - """ Mount an application (:class:`Bottle` or plain WSGI) to a specific - URL prefix. Example:: - - parent_app.mount('/prefix/', child_app) - - :param prefix: path prefix or `mount-point`. - :param app: an instance of :class:`Bottle` or a WSGI application. - - Plugins from the parent application are not applied to the routes - of the mounted child application. If you need plugins in the child - application, install them separately. - - While it is possible to use path wildcards within the prefix path - (:class:`Bottle` childs only), it is highly discouraged. - - The prefix path must end with a slash. If you want to access the - root of the child application via `/prefix` in addition to - `/prefix/`, consider adding a route with a 307 redirect to the - parent application. - """ - - if not prefix.startswith('/'): - raise ValueError("Prefix must start with '/'") - - if isinstance(app, Bottle): - return self._mount_app(prefix, app, **options) - else: - return self._mount_wsgi(prefix, app, **options) - - def merge(self, routes): - """ Merge the routes of another :class:`Bottle` application or a list of - :class:`Route` objects into this application. The routes keep their - 'owner', meaning that the :data:`Route.app` attribute is not - changed. """ - if isinstance(routes, Bottle): - routes = routes.routes - for route in routes: - self.add_route(route) - - def install(self, plugin): - """ Add a plugin to the list of plugins and prepare it for being - applied to all routes of this application. A plugin may be a simple - decorator or an object that implements the :class:`Plugin` API. - """ - if hasattr(plugin, 'setup'): plugin.setup(self) - if not callable(plugin) and not hasattr(plugin, 'apply'): - raise TypeError("Plugins must be callable or implement .apply()") - self.plugins.append(plugin) - self.reset() - return plugin - - def uninstall(self, plugin): - """ Uninstall plugins. Pass an instance to remove a specific plugin, a type - object to remove all plugins that match that type, a string to remove - all plugins with a matching ``name`` attribute or ``True`` to remove all - plugins. Return the list of removed plugins. """ - removed, remove = [], plugin - for i, plugin in list(enumerate(self.plugins))[::-1]: - if remove is True or remove is plugin or remove is type(plugin) \ - or getattr(plugin, 'name', True) == remove: - removed.append(plugin) - del self.plugins[i] - if hasattr(plugin, 'close'): plugin.close() - if removed: self.reset() - return removed - - def reset(self, route=None): - """ Reset all routes (force plugins to be re-applied) and clear all - caches. If an ID or route object is given, only that specific route - is affected. """ - if route is None: routes = self.routes - elif isinstance(route, Route): routes = [route] - else: routes = [self.routes[route]] - for route in routes: - route.reset() - if DEBUG: - for route in routes: - route.prepare() - self.trigger_hook('app_reset') - - def close(self): - """ Close the application and all installed plugins. """ - for plugin in self.plugins: - if hasattr(plugin, 'close'): plugin.close() - - def run(self, **kwargs): - """ Calls :func:`run` with the same parameters. """ - run(self, **kwargs) - - def match(self, environ): - """ Search for a matching route and return a (:class:`Route` , urlargs) - tuple. The second value is a dictionary with parameters extracted - from the URL. Raise :exc:`HTTPError` (404/405) on a non-match.""" - return self.router.match(environ) - - def get_url(self, routename, **kargs): - """ Return a string that matches a named route """ - scriptname = request.environ.get('SCRIPT_NAME', '').strip('/') + '/' - location = self.router.build(routename, **kargs).lstrip('/') - return urljoin(urljoin('/', scriptname), location) - - def add_route(self, route): - """ Add a route object, but do not change the :data:`Route.app` - attribute.""" - self.routes.append(route) - self.router.add(route.rule, route.method, route, name=route.name) - if DEBUG: route.prepare() - - def route(self, - path=None, - method='GET', - callback=None, - name=None, - apply=None, - skip=None, **config): - """ A decorator to bind a function to a request URL. Example:: - - @app.route('/hello/') - def hello(name): - return 'Hello %s' % name - - The ```` part is a wildcard. See :class:`Router` for syntax - details. - - :param path: Request path or a list of paths to listen to. If no - path is specified, it is automatically generated from the - signature of the function. - :param method: HTTP method (`GET`, `POST`, `PUT`, ...) or a list of - methods to listen to. (default: `GET`) - :param callback: An optional shortcut to avoid the decorator - syntax. ``route(..., callback=func)`` equals ``route(...)(func)`` - :param name: The name for this route. (default: None) - :param apply: A decorator or plugin or a list of plugins. These are - applied to the route callback in addition to installed plugins. - :param skip: A list of plugins, plugin classes or names. Matching - plugins are not installed to this route. ``True`` skips all. - - Any additional keyword arguments are stored as route-specific - configuration and passed to plugins (see :meth:`Plugin.apply`). - """ - if callable(path): path, callback = None, path - plugins = makelist(apply) - skiplist = makelist(skip) - - def decorator(callback): - if isinstance(callback, basestring): callback = load(callback) - for rule in makelist(path) or yieldroutes(callback): - for verb in makelist(method): - verb = verb.upper() - route = Route(self, rule, verb, callback, - name=name, - plugins=plugins, - skiplist=skiplist, **config) - self.add_route(route) - return callback - - return decorator(callback) if callback else decorator - - def get(self, path=None, method='GET', **options): - """ Equals :meth:`route`. """ - return self.route(path, method, **options) - - def post(self, path=None, method='POST', **options): - """ Equals :meth:`route` with a ``POST`` method parameter. """ - return self.route(path, method, **options) - - def put(self, path=None, method='PUT', **options): - """ Equals :meth:`route` with a ``PUT`` method parameter. """ - return self.route(path, method, **options) - - def delete(self, path=None, method='DELETE', **options): - """ Equals :meth:`route` with a ``DELETE`` method parameter. """ - return self.route(path, method, **options) - - def patch(self, path=None, method='PATCH', **options): - """ Equals :meth:`route` with a ``PATCH`` method parameter. """ - return self.route(path, method, **options) - - def error(self, code=500, callback=None): - """ Register an output handler for a HTTP error code. Can - be used as a decorator or called directly :: - - def error_handler_500(error): - return 'error_handler_500' - - app.error(code=500, callback=error_handler_500) - - @app.error(404) - def error_handler_404(error): - return 'error_handler_404' - - """ - - def decorator(callback): - if isinstance(callback, basestring): callback = load(callback) - self.error_handler[int(code)] = callback - return callback - - return decorator(callback) if callback else decorator - - def default_error_handler(self, res): - return tob(template(ERROR_PAGE_TEMPLATE, e=res, template_settings=dict(name='__ERROR_PAGE_TEMPLATE'))) - - def _handle(self, environ): - path = environ['bottle.raw_path'] = environ['PATH_INFO'] - if py3k: - environ['PATH_INFO'] = path.encode('latin1').decode('utf8', 'ignore') - - environ['bottle.app'] = self - request.bind(environ) - response.bind() - - try: - while True: # Remove in 0.14 together with RouteReset - out = None - try: - self.trigger_hook('before_request') - route, args = self.router.match(environ) - environ['route.handle'] = route - environ['bottle.route'] = route - environ['route.url_args'] = args - out = route.call(**args) - break - except HTTPResponse as E: - out = E - break - except RouteReset: - depr(0, 13, "RouteReset exception deprecated", - "Call route.call() after route.reset() and " - "return the result.") - route.reset() - continue - finally: - if isinstance(out, HTTPResponse): - out.apply(response) - try: - self.trigger_hook('after_request') - except HTTPResponse as E: - out = E - out.apply(response) - except (KeyboardInterrupt, SystemExit, MemoryError): - raise - except Exception as E: - if not self.catchall: raise - stacktrace = format_exc() - environ['wsgi.errors'].write(stacktrace) - environ['wsgi.errors'].flush() - out = HTTPError(500, "Internal Server Error", E, stacktrace) - out.apply(response) - - return out - - def _cast(self, out, peek=None): - """ Try to convert the parameter into something WSGI compatible and set - correct HTTP headers when possible. - Support: False, str, unicode, dict, HTTPResponse, HTTPError, file-like, - iterable of strings and iterable of unicodes - """ - - # Empty output is done here - if not out: - if 'Content-Length' not in response: - response['Content-Length'] = 0 - return [] - # Join lists of byte or unicode strings. Mixed lists are NOT supported - if isinstance(out, (tuple, list))\ - and isinstance(out[0], (bytes, unicode)): - out = out[0][0:0].join(out) # b'abc'[0:0] -> b'' - # Encode unicode strings - if isinstance(out, unicode): - out = out.encode(response.charset) - # Byte Strings are just returned - if isinstance(out, bytes): - if 'Content-Length' not in response: - response['Content-Length'] = len(out) - return [out] - # HTTPError or HTTPException (recursive, because they may wrap anything) - # TODO: Handle these explicitly in handle() or make them iterable. - if isinstance(out, HTTPError): - out.apply(response) - out = self.error_handler.get(out.status_code, - self.default_error_handler)(out) - return self._cast(out) - if isinstance(out, HTTPResponse): - out.apply(response) - return self._cast(out.body) - - # File-like objects. - if hasattr(out, 'read'): - if 'wsgi.file_wrapper' in request.environ: - return request.environ['wsgi.file_wrapper'](out) - elif hasattr(out, 'close') or not hasattr(out, '__iter__'): - return WSGIFileWrapper(out) - - # Handle Iterables. We peek into them to detect their inner type. - try: - iout = iter(out) - first = next(iout) - while not first: - first = next(iout) - except StopIteration: - return self._cast('') - except HTTPResponse as E: - first = E - except (KeyboardInterrupt, SystemExit, MemoryError): - raise - except Exception as error: - if not self.catchall: raise - first = HTTPError(500, 'Unhandled exception', error, format_exc()) - - # These are the inner types allowed in iterator or generator objects. - if isinstance(first, HTTPResponse): - return self._cast(first) - elif isinstance(first, bytes): - new_iter = itertools.chain([first], iout) - elif isinstance(first, unicode): - encoder = lambda x: x.encode(response.charset) - new_iter = imap(encoder, itertools.chain([first], iout)) - else: - msg = 'Unsupported response type: %s' % type(first) - return self._cast(HTTPError(500, msg)) - if hasattr(out, 'close'): - new_iter = _closeiter(new_iter, out.close) - return new_iter - - def wsgi(self, environ, start_response): - """ The bottle WSGI-interface. """ - try: - out = self._cast(self._handle(environ)) - # rfc2616 section 4.3 - if response._status_code in (100, 101, 204, 304)\ - or environ['REQUEST_METHOD'] == 'HEAD': - if hasattr(out, 'close'): out.close() - out = [] - start_response(response._status_line, response.headerlist) - return out - except (KeyboardInterrupt, SystemExit, MemoryError): - raise - except Exception as E: - if not self.catchall: raise - err = '

Critical error while processing request: %s

' \ - % html_escape(environ.get('PATH_INFO', '/')) - if DEBUG: - err += '

Error:

\n
\n%s\n
\n' \ - '

Traceback:

\n
\n%s\n
\n' \ - % (html_escape(repr(E)), html_escape(format_exc())) - environ['wsgi.errors'].write(err) - environ['wsgi.errors'].flush() - headers = [('Content-Type', 'text/html; charset=UTF-8')] - start_response('500 INTERNAL SERVER ERROR', headers, sys.exc_info()) - return [tob(err)] - - def __call__(self, environ, start_response): - """ Each instance of :class:'Bottle' is a WSGI application. """ - return self.wsgi(environ, start_response) - - def __enter__(self): - """ Use this application as default for all module-level shortcuts. """ - default_app.push(self) - return self - - def __exit__(self, exc_type, exc_value, traceback): - default_app.pop() - - def __setattr__(self, name, value): - if name in self.__dict__: - raise AttributeError("Attribute %s already defined. Plugin conflict?" % name) - self.__dict__[name] = value - - -############################################################################### -# HTTP and WSGI Tools ########################################################## -############################################################################### - - -class BaseRequest(object): - """ A wrapper for WSGI environment dictionaries that adds a lot of - convenient access methods and properties. Most of them are read-only. - - Adding new attributes to a request actually adds them to the environ - dictionary (as 'bottle.request.ext.'). This is the recommended - way to store and access request-specific data. - """ - - __slots__ = ('environ', ) - - #: Maximum size of memory buffer for :attr:`body` in bytes. - MEMFILE_MAX = 102400 - - def __init__(self, environ=None): - """ Wrap a WSGI environ dictionary. """ - #: The wrapped WSGI environ dictionary. This is the only real attribute. - #: All other attributes actually are read-only properties. - self.environ = {} if environ is None else environ - self.environ['bottle.request'] = self - - @DictProperty('environ', 'bottle.app', read_only=True) - def app(self): - """ Bottle application handling this request. """ - raise RuntimeError('This request is not connected to an application.') - - @DictProperty('environ', 'bottle.route', read_only=True) - def route(self): - """ The bottle :class:`Route` object that matches this request. """ - raise RuntimeError('This request is not connected to a route.') - - @DictProperty('environ', 'route.url_args', read_only=True) - def url_args(self): - """ The arguments extracted from the URL. """ - raise RuntimeError('This request is not connected to a route.') - - @property - def path(self): - """ The value of ``PATH_INFO`` with exactly one prefixed slash (to fix - broken clients and avoid the "empty path" edge case). """ - return '/' + self.environ.get('PATH_INFO', '').lstrip('/') - - @property - def method(self): - """ The ``REQUEST_METHOD`` value as an uppercase string. """ - return self.environ.get('REQUEST_METHOD', 'GET').upper() - - @DictProperty('environ', 'bottle.request.headers', read_only=True) - def headers(self): - """ A :class:`WSGIHeaderDict` that provides case-insensitive access to - HTTP request headers. """ - return WSGIHeaderDict(self.environ) - - def get_header(self, name, default=None): - """ Return the value of a request header, or a given default value. """ - return self.headers.get(name, default) - - @DictProperty('environ', 'bottle.request.cookies', read_only=True) - def cookies(self): - """ Cookies parsed into a :class:`FormsDict`. Signed cookies are NOT - decoded. Use :meth:`get_cookie` if you expect signed cookies. """ - cookies = SimpleCookie(self.environ.get('HTTP_COOKIE', '')).values() - return FormsDict((c.key, c.value) for c in cookies) - - def get_cookie(self, key, default=None, secret=None, digestmod=hashlib.sha256): - """ Return the content of a cookie. To read a `Signed Cookie`, the - `secret` must match the one used to create the cookie (see - :meth:`BaseResponse.set_cookie`). If anything goes wrong (missing - cookie or wrong signature), return a default value. """ - value = self.cookies.get(key) - if secret: - # See BaseResponse.set_cookie for details on signed cookies. - if value and value.startswith('!') and '?' in value: - sig, msg = map(tob, value[1:].split('?', 1)) - hash = hmac.new(tob(secret), msg, digestmod=digestmod).digest() - if _lscmp(sig, base64.b64encode(hash)): - dst = pickle.loads(base64.b64decode(msg)) - if dst and dst[0] == key: - return dst[1] - return default - return value or default - - @DictProperty('environ', 'bottle.request.query', read_only=True) - def query(self): - """ The :attr:`query_string` parsed into a :class:`FormsDict`. These - values are sometimes called "URL arguments" or "GET parameters", but - not to be confused with "URL wildcards" as they are provided by the - :class:`Router`. """ - get = self.environ['bottle.get'] = FormsDict() - pairs = _parse_qsl(self.environ.get('QUERY_STRING', '')) - for key, value in pairs: - get[key] = value - return get - - @DictProperty('environ', 'bottle.request.forms', read_only=True) - def forms(self): - """ Form values parsed from an `url-encoded` or `multipart/form-data` - encoded POST or PUT request body. The result is returned as a - :class:`FormsDict`. All keys and values are strings. File uploads - are stored separately in :attr:`files`. """ - forms = FormsDict() - forms.recode_unicode = self.POST.recode_unicode - for name, item in self.POST.allitems(): - if not isinstance(item, FileUpload): - forms[name] = item - return forms - - @DictProperty('environ', 'bottle.request.params', read_only=True) - def params(self): - """ A :class:`FormsDict` with the combined values of :attr:`query` and - :attr:`forms`. File uploads are stored in :attr:`files`. """ - params = FormsDict() - for key, value in self.query.allitems(): - params[key] = value - for key, value in self.forms.allitems(): - params[key] = value - return params - - @DictProperty('environ', 'bottle.request.files', read_only=True) - def files(self): - """ File uploads parsed from `multipart/form-data` encoded POST or PUT - request body. The values are instances of :class:`FileUpload`. - - """ - files = FormsDict() - files.recode_unicode = self.POST.recode_unicode - for name, item in self.POST.allitems(): - if isinstance(item, FileUpload): - files[name] = item - return files - - @DictProperty('environ', 'bottle.request.json', read_only=True) - def json(self): - """ If the ``Content-Type`` header is ``application/json`` or - ``application/json-rpc``, this property holds the parsed content - of the request body. Only requests smaller than :attr:`MEMFILE_MAX` - are processed to avoid memory exhaustion. - Invalid JSON raises a 400 error response. - """ - ctype = self.environ.get('CONTENT_TYPE', '').lower().split(';')[0] - if ctype in ('application/json', 'application/json-rpc'): - b = self._get_body_string() - if not b: - return None - try: - return json_loads(b) - except (ValueError, TypeError): - raise HTTPError(400, 'Invalid JSON') - return None - - def _iter_body(self, read, bufsize): - maxread = max(0, self.content_length) - while maxread: - part = read(min(maxread, bufsize)) - if not part: break - yield part - maxread -= len(part) - - @staticmethod - def _iter_chunked(read, bufsize): - err = HTTPError(400, 'Error while parsing chunked transfer body.') - rn, sem, bs = tob('\r\n'), tob(';'), tob('') - while True: - header = read(1) - while header[-2:] != rn: - c = read(1) - header += c - if not c: raise err - if len(header) > bufsize: raise err - size, _, _ = header.partition(sem) - try: - maxread = int(tonat(size.strip()), 16) - except ValueError: - raise err - if maxread == 0: break - buff = bs - while maxread > 0: - if not buff: - buff = read(min(maxread, bufsize)) - part, buff = buff[:maxread], buff[maxread:] - if not part: raise err - yield part - maxread -= len(part) - if read(2) != rn: - raise err - - @DictProperty('environ', 'bottle.request.body', read_only=True) - def _body(self): - try: - read_func = self.environ['wsgi.input'].read - except KeyError: - self.environ['wsgi.input'] = BytesIO() - return self.environ['wsgi.input'] - body_iter = self._iter_chunked if self.chunked else self._iter_body - body, body_size, is_temp_file = BytesIO(), 0, False - for part in body_iter(read_func, self.MEMFILE_MAX): - body.write(part) - body_size += len(part) - if not is_temp_file and body_size > self.MEMFILE_MAX: - body, tmp = TemporaryFile(mode='w+b'), body - body.write(tmp.getvalue()) - del tmp - is_temp_file = True - self.environ['wsgi.input'] = body - body.seek(0) - return body - - def _get_body_string(self): - """ read body until content-length or MEMFILE_MAX into a string. Raise - HTTPError(413) on requests that are to large. """ - clen = self.content_length - if clen > self.MEMFILE_MAX: - raise HTTPError(413, 'Request entity too large') - if clen < 0: clen = self.MEMFILE_MAX + 1 - data = self.body.read(clen) - if len(data) > self.MEMFILE_MAX: # Fail fast - raise HTTPError(413, 'Request entity too large') - return data - - @property - def body(self): - """ The HTTP request body as a seek-able file-like object. Depending on - :attr:`MEMFILE_MAX`, this is either a temporary file or a - :class:`io.BytesIO` instance. Accessing this property for the first - time reads and replaces the ``wsgi.input`` environ variable. - Subsequent accesses just do a `seek(0)` on the file object. """ - self._body.seek(0) - return self._body - - @property - def chunked(self): - """ True if Chunked transfer encoding was. """ - return 'chunked' in self.environ.get( - 'HTTP_TRANSFER_ENCODING', '').lower() - - #: An alias for :attr:`query`. - GET = query - - @DictProperty('environ', 'bottle.request.post', read_only=True) - def POST(self): - """ The values of :attr:`forms` and :attr:`files` combined into a single - :class:`FormsDict`. Values are either strings (form values) or - instances of :class:`cgi.FieldStorage` (file uploads). - """ - post = FormsDict() - # We default to application/x-www-form-urlencoded for everything that - # is not multipart and take the fast path (also: 3.1 workaround) - if not self.content_type.startswith('multipart/'): - pairs = _parse_qsl(tonat(self._get_body_string(), 'latin1')) - for key, value in pairs: - post[key] = value - return post - - safe_env = {'QUERY_STRING': ''} # Build a safe environment for cgi - for key in ('REQUEST_METHOD', 'CONTENT_TYPE', 'CONTENT_LENGTH'): - if key in self.environ: safe_env[key] = self.environ[key] - args = dict(fp=self.body, environ=safe_env, keep_blank_values=True) - - if py3k: - args['encoding'] = 'utf8' - post.recode_unicode = False - data = cgi.FieldStorage(**args) - self['_cgi.FieldStorage'] = data #http://bugs.python.org/issue18394 - data = data.list or [] - for item in data: - if item.filename: - post[item.name] = FileUpload(item.file, item.name, - item.filename, item.headers) - else: - post[item.name] = item.value - return post - - @property - def url(self): - """ The full request URI including hostname and scheme. If your app - lives behind a reverse proxy or load balancer and you get confusing - results, make sure that the ``X-Forwarded-Host`` header is set - correctly. """ - return self.urlparts.geturl() - - @DictProperty('environ', 'bottle.request.urlparts', read_only=True) - def urlparts(self): - """ The :attr:`url` string as an :class:`urlparse.SplitResult` tuple. - The tuple contains (scheme, host, path, query_string and fragment), - but the fragment is always empty because it is not visible to the - server. """ - env = self.environ - http = env.get('HTTP_X_FORWARDED_PROTO') \ - or env.get('wsgi.url_scheme', 'http') - host = env.get('HTTP_X_FORWARDED_HOST') or env.get('HTTP_HOST') - if not host: - # HTTP 1.1 requires a Host-header. This is for HTTP/1.0 clients. - host = env.get('SERVER_NAME', '127.0.0.1') - port = env.get('SERVER_PORT') - if port and port != ('80' if http == 'http' else '443'): - host += ':' + port - path = urlquote(self.fullpath) - return UrlSplitResult(http, host, path, env.get('QUERY_STRING'), '') - - @property - def fullpath(self): - """ Request path including :attr:`script_name` (if present). """ - return urljoin(self.script_name, self.path.lstrip('/')) - - @property - def query_string(self): - """ The raw :attr:`query` part of the URL (everything in between ``?`` - and ``#``) as a string. """ - return self.environ.get('QUERY_STRING', '') - - @property - def script_name(self): - """ The initial portion of the URL's `path` that was removed by a higher - level (server or routing middleware) before the application was - called. This script path is returned with leading and tailing - slashes. """ - script_name = self.environ.get('SCRIPT_NAME', '').strip('/') - return '/' + script_name + '/' if script_name else '/' - - def path_shift(self, shift=1): - """ Shift path segments from :attr:`path` to :attr:`script_name` and - vice versa. - - :param shift: The number of path segments to shift. May be negative - to change the shift direction. (default: 1) - """ - script, path = path_shift(self.environ.get('SCRIPT_NAME', '/'), self.path, shift) - self['SCRIPT_NAME'], self['PATH_INFO'] = script, path - - @property - def content_length(self): - """ The request body length as an integer. The client is responsible to - set this header. Otherwise, the real length of the body is unknown - and -1 is returned. In this case, :attr:`body` will be empty. """ - return int(self.environ.get('CONTENT_LENGTH') or -1) - - @property - def content_type(self): - """ The Content-Type header as a lowercase-string (default: empty). """ - return self.environ.get('CONTENT_TYPE', '').lower() - - @property - def is_xhr(self): - """ True if the request was triggered by a XMLHttpRequest. This only - works with JavaScript libraries that support the `X-Requested-With` - header (most of the popular libraries do). """ - requested_with = self.environ.get('HTTP_X_REQUESTED_WITH', '') - return requested_with.lower() == 'xmlhttprequest' - - @property - def is_ajax(self): - """ Alias for :attr:`is_xhr`. "Ajax" is not the right term. """ - return self.is_xhr - - @property - def auth(self): - """ HTTP authentication data as a (user, password) tuple. This - implementation currently supports basic (not digest) authentication - only. If the authentication happened at a higher level (e.g. in the - front web-server or a middleware), the password field is None, but - the user field is looked up from the ``REMOTE_USER`` environ - variable. On any errors, None is returned. """ - basic = parse_auth(self.environ.get('HTTP_AUTHORIZATION', '')) - if basic: return basic - ruser = self.environ.get('REMOTE_USER') - if ruser: return (ruser, None) - return None - - @property - def remote_route(self): - """ A list of all IPs that were involved in this request, starting with - the client IP and followed by zero or more proxies. This does only - work if all proxies support the ```X-Forwarded-For`` header. Note - that this information can be forged by malicious clients. """ - proxy = self.environ.get('HTTP_X_FORWARDED_FOR') - if proxy: return [ip.strip() for ip in proxy.split(',')] - remote = self.environ.get('REMOTE_ADDR') - return [remote] if remote else [] - - @property - def remote_addr(self): - """ The client IP as a string. Note that this information can be forged - by malicious clients. """ - route = self.remote_route - return route[0] if route else None - - def copy(self): - """ Return a new :class:`Request` with a shallow :attr:`environ` copy. """ - return Request(self.environ.copy()) - - def get(self, value, default=None): - return self.environ.get(value, default) - - def __getitem__(self, key): - return self.environ[key] - - def __delitem__(self, key): - self[key] = "" - del (self.environ[key]) - - def __iter__(self): - return iter(self.environ) - - def __len__(self): - return len(self.environ) - - def keys(self): - return self.environ.keys() - - def __setitem__(self, key, value): - """ Change an environ value and clear all caches that depend on it. """ - - if self.environ.get('bottle.request.readonly'): - raise KeyError('The environ dictionary is read-only.') - - self.environ[key] = value - todelete = () - - if key == 'wsgi.input': - todelete = ('body', 'forms', 'files', 'params', 'post', 'json') - elif key == 'QUERY_STRING': - todelete = ('query', 'params') - elif key.startswith('HTTP_'): - todelete = ('headers', 'cookies') - - for key in todelete: - self.environ.pop('bottle.request.' + key, None) - - def __repr__(self): - return '<%s: %s %s>' % (self.__class__.__name__, self.method, self.url) - - def __getattr__(self, name): - """ Search in self.environ for additional user defined attributes. """ - try: - var = self.environ['bottle.request.ext.%s' % name] - return var.__get__(self) if hasattr(var, '__get__') else var - except KeyError: - raise AttributeError('Attribute %r not defined.' % name) - - def __setattr__(self, name, value): - if name == 'environ': return object.__setattr__(self, name, value) - key = 'bottle.request.ext.%s' % name - if key in self.environ: - raise AttributeError("Attribute already defined: %s" % name) - self.environ[key] = value - - def __delattr__(self, name): - try: - del self.environ['bottle.request.ext.%s' % name] - except KeyError: - raise AttributeError("Attribute not defined: %s" % name) - - -def _hkey(key): - if '\n' in key or '\r' in key or '\0' in key: - raise ValueError("Header names must not contain control characters: %r" % key) - return key.title().replace('_', '-') - - -def _hval(value): - value = tonat(value) - if '\n' in value or '\r' in value or '\0' in value: - raise ValueError("Header value must not contain control characters: %r" % value) - return value - - -class HeaderProperty(object): - def __init__(self, name, reader=None, writer=None, default=''): - self.name, self.default = name, default - self.reader, self.writer = reader, writer - self.__doc__ = 'Current value of the %r header.' % name.title() - - def __get__(self, obj, _): - if obj is None: return self - value = obj.get_header(self.name, self.default) - return self.reader(value) if self.reader else value - - def __set__(self, obj, value): - obj[self.name] = self.writer(value) if self.writer else value - - def __delete__(self, obj): - del obj[self.name] - - -class BaseResponse(object): - """ Storage class for a response body as well as headers and cookies. - - This class does support dict-like case-insensitive item-access to - headers, but is NOT a dict. Most notably, iterating over a response - yields parts of the body and not the headers. - - :param body: The response body as one of the supported types. - :param status: Either an HTTP status code (e.g. 200) or a status line - including the reason phrase (e.g. '200 OK'). - :param headers: A dictionary or a list of name-value pairs. - - Additional keyword arguments are added to the list of headers. - Underscores in the header name are replaced with dashes. - """ - - default_status = 200 - default_content_type = 'text/html; charset=UTF-8' - - # Header blacklist for specific response codes - # (rfc2616 section 10.2.3 and 10.3.5) - bad_headers = { - 204: frozenset(('Content-Type', 'Content-Length')), - 304: frozenset(('Allow', 'Content-Encoding', 'Content-Language', - 'Content-Length', 'Content-Range', 'Content-Type', - 'Content-Md5', 'Last-Modified')) - } - - def __init__(self, body='', status=None, headers=None, **more_headers): - self._cookies = None - self._headers = {} - self.body = body - self.status = status or self.default_status - if headers: - if isinstance(headers, dict): - headers = headers.items() - for name, value in headers: - self.add_header(name, value) - if more_headers: - for name, value in more_headers.items(): - self.add_header(name, value) - - def copy(self, cls=None): - """ Returns a copy of self. """ - cls = cls or BaseResponse - assert issubclass(cls, BaseResponse) - copy = cls() - copy.status = self.status - copy._headers = dict((k, v[:]) for (k, v) in self._headers.items()) - if self._cookies: - cookies = copy._cookies = SimpleCookie() - for k,v in self._cookies.items(): - cookies[k] = v.value - cookies[k].update(v) # also copy cookie attributes - return copy - - def __iter__(self): - return iter(self.body) - - def close(self): - if hasattr(self.body, 'close'): - self.body.close() - - @property - def status_line(self): - """ The HTTP status line as a string (e.g. ``404 Not Found``).""" - return self._status_line - - @property - def status_code(self): - """ The HTTP status code as an integer (e.g. 404).""" - return self._status_code - - def _set_status(self, status): - if isinstance(status, int): - code, status = status, _HTTP_STATUS_LINES.get(status) - elif ' ' in status: - status = status.strip() - code = int(status.split()[0]) - else: - raise ValueError('String status line without a reason phrase.') - if not 100 <= code <= 999: - raise ValueError('Status code out of range.') - self._status_code = code - self._status_line = str(status or ('%d Unknown' % code)) - - def _get_status(self): - return self._status_line - - status = property( - _get_status, _set_status, None, - ''' A writeable property to change the HTTP response status. It accepts - either a numeric code (100-999) or a string with a custom reason - phrase (e.g. "404 Brain not found"). Both :data:`status_line` and - :data:`status_code` are updated accordingly. The return value is - always a status string. ''') - del _get_status, _set_status - - @property - def headers(self): - """ An instance of :class:`HeaderDict`, a case-insensitive dict-like - view on the response headers. """ - hdict = HeaderDict() - hdict.dict = self._headers - return hdict - - def __contains__(self, name): - return _hkey(name) in self._headers - - def __delitem__(self, name): - del self._headers[_hkey(name)] - - def __getitem__(self, name): - return self._headers[_hkey(name)][-1] - - def __setitem__(self, name, value): - self._headers[_hkey(name)] = [_hval(value)] - - def get_header(self, name, default=None): - """ Return the value of a previously defined header. If there is no - header with that name, return a default value. """ - return self._headers.get(_hkey(name), [default])[-1] - - def set_header(self, name, value): - """ Create a new response header, replacing any previously defined - headers with the same name. """ - self._headers[_hkey(name)] = [_hval(value)] - - def add_header(self, name, value): - """ Add an additional response header, not removing duplicates. """ - self._headers.setdefault(_hkey(name), []).append(_hval(value)) - - def iter_headers(self): - """ Yield (header, value) tuples, skipping headers that are not - allowed with the current response status code. """ - return self.headerlist - - @property - def headerlist(self): - """ WSGI conform list of (header, value) tuples. """ - out = [] - headers = list(self._headers.items()) - if 'Content-Type' not in self._headers: - headers.append(('Content-Type', [self.default_content_type])) - if self._status_code in self.bad_headers: - bad_headers = self.bad_headers[self._status_code] - headers = [h for h in headers if h[0] not in bad_headers] - out += [(name, val) for (name, vals) in headers for val in vals] - if self._cookies: - for c in self._cookies.values(): - out.append(('Set-Cookie', _hval(c.OutputString()))) - if py3k: - out = [(k, v.encode('utf8').decode('latin1')) for (k, v) in out] - return out - - content_type = HeaderProperty('Content-Type') - content_length = HeaderProperty('Content-Length', reader=int, default=-1) - expires = HeaderProperty( - 'Expires', - reader=lambda x: datetime.utcfromtimestamp(parse_date(x)), - writer=lambda x: http_date(x)) - - @property - def charset(self, default='UTF-8'): - """ Return the charset specified in the content-type header (default: utf8). """ - if 'charset=' in self.content_type: - return self.content_type.split('charset=')[-1].split(';')[0].strip() - return default - - def set_cookie(self, name, value, secret=None, digestmod=hashlib.sha256, **options): - """ Create a new cookie or replace an old one. If the `secret` parameter is - set, create a `Signed Cookie` (described below). - - :param name: the name of the cookie. - :param value: the value of the cookie. - :param secret: a signature key required for signed cookies. - - Additionally, this method accepts all RFC 2109 attributes that are - supported by :class:`cookie.Morsel`, including: - - :param maxage: maximum age in seconds. (default: None) - :param expires: a datetime object or UNIX timestamp. (default: None) - :param domain: the domain that is allowed to read the cookie. - (default: current domain) - :param path: limits the cookie to a given path (default: current path) - :param secure: limit the cookie to HTTPS connections (default: off). - :param httponly: prevents client-side javascript to read this cookie - (default: off, requires Python 2.6 or newer). - :param samesite: disables third-party use for a cookie. - Allowed attributes: `lax` and `strict`. - In strict mode the cookie will never be sent. - In lax mode the cookie is only sent with a top-level GET request. - - If neither `expires` nor `maxage` is set (default), the cookie will - expire at the end of the browser session (as soon as the browser - window is closed). - - Signed cookies may store any pickle-able object and are - cryptographically signed to prevent manipulation. Keep in mind that - cookies are limited to 4kb in most browsers. - - Warning: Pickle is a potentially dangerous format. If an attacker - gains access to the secret key, he could forge cookies that execute - code on server side if unpickeld. Using pickle is discouraged and - support for it will be removed in later versions of bottle. - - Warning: Signed cookies are not encrypted (the client can still see - the content) and not copy-protected (the client can restore an old - cookie). The main intention is to make pickling and unpickling - save, not to store secret information at client side. - """ - if not self._cookies: - self._cookies = SimpleCookie() - - # Monkey-patch Cookie lib to support 'SameSite' parameter - # https://tools.ietf.org/html/draft-west-first-party-cookies-07#section-4.1 - Morsel._reserved.setdefault('samesite', 'SameSite') - - if secret: - if not isinstance(value, basestring): - depr(0, 13, "Pickling of arbitrary objects into cookies is " - "deprecated.", "Only store strings in cookies. " - "JSON strings are fine, too.") - encoded = base64.b64encode(pickle.dumps([name, value], -1)) - sig = base64.b64encode(hmac.new(tob(secret), encoded, - digestmod=digestmod).digest()) - value = touni(tob('!') + sig + tob('?') + encoded) - elif not isinstance(value, basestring): - raise TypeError('Secret key required for non-string cookies.') - - # Cookie size plus options must not exceed 4kb. - if len(name) + len(value) > 3800: - raise ValueError('Content does not fit into a cookie.') - - self._cookies[name] = value - - for key, value in options.items(): - if key in ('max_age', 'maxage'): # 'maxage' variant added in 0.13 - key = 'max-age' - if isinstance(value, timedelta): - value = value.seconds + value.days * 24 * 3600 - if key == 'expires': - if isinstance(value, (datedate, datetime)): - value = value.timetuple() - elif isinstance(value, (int, float)): - value = time.gmtime(value) - value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) - if key in ('same_site', 'samesite'): # 'samesite' variant added in 0.13 - key = 'samesite' - if value.lower() not in ('lax', 'strict'): - raise CookieError("Invalid value samesite=%r (expected 'lax' or 'strict')" % (key,)) - if key in ('secure', 'httponly') and not value: - continue - self._cookies[name][key] = value - - def delete_cookie(self, key, **kwargs): - """ Delete a cookie. Be sure to use the same `domain` and `path` - settings as used to create the cookie. """ - kwargs['max_age'] = -1 - kwargs['expires'] = 0 - self.set_cookie(key, '', **kwargs) - - def __repr__(self): - out = '' - for name, value in self.headerlist: - out += '%s: %s\n' % (name.title(), value.strip()) - return out - - -def _local_property(): - ls = threading.local() - - def fget(_): - try: - return ls.var - except AttributeError: - raise RuntimeError("Request context not initialized.") - - def fset(_, value): - ls.var = value - - def fdel(_): - del ls.var - - return property(fget, fset, fdel, 'Thread-local property') - - -class LocalRequest(BaseRequest): - """ A thread-local subclass of :class:`BaseRequest` with a different - set of attributes for each thread. There is usually only one global - instance of this class (:data:`request`). If accessed during a - request/response cycle, this instance always refers to the *current* - request (even on a multithreaded server). """ - bind = BaseRequest.__init__ - environ = _local_property() - - -class LocalResponse(BaseResponse): - """ A thread-local subclass of :class:`BaseResponse` with a different - set of attributes for each thread. There is usually only one global - instance of this class (:data:`response`). Its attributes are used - to build the HTTP response at the end of the request/response cycle. - """ - bind = BaseResponse.__init__ - _status_line = _local_property() - _status_code = _local_property() - _cookies = _local_property() - _headers = _local_property() - body = _local_property() - - -Request = BaseRequest -Response = BaseResponse - - -class HTTPResponse(Response, BottleException): - def __init__(self, body='', status=None, headers=None, **more_headers): - super(HTTPResponse, self).__init__(body, status, headers, **more_headers) - - def apply(self, other): - other._status_code = self._status_code - other._status_line = self._status_line - other._headers = self._headers - other._cookies = self._cookies - other.body = self.body - - -class HTTPError(HTTPResponse): - default_status = 500 - - def __init__(self, - status=None, - body=None, - exception=None, - traceback=None, **more_headers): - self.exception = exception - self.traceback = traceback - super(HTTPError, self).__init__(body, status, **more_headers) - -############################################################################### -# Plugins ###################################################################### -############################################################################### - - -class PluginError(BottleException): - pass - - -class JSONPlugin(object): - name = 'json' - api = 2 - - def __init__(self, json_dumps=json_dumps): - self.json_dumps = json_dumps - - def setup(self, app): - app.config._define('json.enable', default=True, validate=bool, - help="Enable or disable automatic dict->json filter.") - app.config._define('json.ascii', default=False, validate=bool, - help="Use only 7-bit ASCII characters in output.") - app.config._define('json.indent', default=True, validate=bool, - help="Add whitespace to make json more readable.") - app.config._define('json.dump_func', default=None, - help="If defined, use this function to transform" - " dict into json. The other options no longer" - " apply.") - - def apply(self, callback, route): - dumps = self.json_dumps - if not self.json_dumps: return callback - - def wrapper(*a, **ka): - try: - rv = callback(*a, **ka) - except HTTPResponse as resp: - rv = resp - - if isinstance(rv, dict): - #Attempt to serialize, raises exception on failure - json_response = dumps(rv) - #Set content type only if serialization successful - response.content_type = 'application/json' - return json_response - elif isinstance(rv, HTTPResponse) and isinstance(rv.body, dict): - rv.body = dumps(rv.body) - rv.content_type = 'application/json' - return rv - - return wrapper - - -class TemplatePlugin(object): - """ This plugin applies the :func:`view` decorator to all routes with a - `template` config parameter. If the parameter is a tuple, the second - element must be a dict with additional options (e.g. `template_engine`) - or default variables for the template. """ - name = 'template' - api = 2 - - def setup(self, app): - app.tpl = self - - def apply(self, callback, route): - conf = route.config.get('template') - if isinstance(conf, (tuple, list)) and len(conf) == 2: - return view(conf[0], **conf[1])(callback) - elif isinstance(conf, str): - return view(conf)(callback) - else: - return callback - - -#: Not a plugin, but part of the plugin API. TODO: Find a better place. -class _ImportRedirect(object): - def __init__(self, name, impmask): - """ Create a virtual package that redirects imports (see PEP 302). """ - self.name = name - self.impmask = impmask - self.module = sys.modules.setdefault(name, imp.new_module(name)) - self.module.__dict__.update({ - '__file__': __file__, - '__path__': [], - '__all__': [], - '__loader__': self - }) - sys.meta_path.append(self) - - def find_module(self, fullname, path=None): - if '.' not in fullname: return - packname = fullname.rsplit('.', 1)[0] - if packname != self.name: return - return self - - def load_module(self, fullname): - if fullname in sys.modules: return sys.modules[fullname] - modname = fullname.rsplit('.', 1)[1] - realname = self.impmask % modname - __import__(realname) - module = sys.modules[fullname] = sys.modules[realname] - setattr(self.module, modname, module) - module.__loader__ = self - return module - -############################################################################### -# Common Utilities ############################################################# -############################################################################### - - -class MultiDict(DictMixin): - """ This dict stores multiple values per key, but behaves exactly like a - normal dict in that it returns only the newest value for any given key. - There are special methods available to access the full list of values. - """ - - def __init__(self, *a, **k): - self.dict = dict((k, [v]) for (k, v) in dict(*a, **k).items()) - - def __len__(self): - return len(self.dict) - - def __iter__(self): - return iter(self.dict) - - def __contains__(self, key): - return key in self.dict - - def __delitem__(self, key): - del self.dict[key] - - def __getitem__(self, key): - return self.dict[key][-1] - - def __setitem__(self, key, value): - self.append(key, value) - - def keys(self): - return self.dict.keys() - - if py3k: - - def values(self): - return (v[-1] for v in self.dict.values()) - - def items(self): - return ((k, v[-1]) for k, v in self.dict.items()) - - def allitems(self): - return ((k, v) for k, vl in self.dict.items() for v in vl) - - iterkeys = keys - itervalues = values - iteritems = items - iterallitems = allitems - - else: - - def values(self): - return [v[-1] for v in self.dict.values()] - - def items(self): - return [(k, v[-1]) for k, v in self.dict.items()] - - def iterkeys(self): - return self.dict.iterkeys() - - def itervalues(self): - return (v[-1] for v in self.dict.itervalues()) - - def iteritems(self): - return ((k, v[-1]) for k, v in self.dict.iteritems()) - - def iterallitems(self): - return ((k, v) for k, vl in self.dict.iteritems() for v in vl) - - def allitems(self): - return [(k, v) for k, vl in self.dict.iteritems() for v in vl] - - def get(self, key, default=None, index=-1, type=None): - """ Return the most recent value for a key. - - :param default: The default value to be returned if the key is not - present or the type conversion fails. - :param index: An index for the list of available values. - :param type: If defined, this callable is used to cast the value - into a specific type. Exception are suppressed and result in - the default value to be returned. - """ - try: - val = self.dict[key][index] - return type(val) if type else val - except Exception: - pass - return default - - def append(self, key, value): - """ Add a new value to the list of values for this key. """ - self.dict.setdefault(key, []).append(value) - - def replace(self, key, value): - """ Replace the list of values with a single value. """ - self.dict[key] = [value] - - def getall(self, key): - """ Return a (possibly empty) list of values for a key. """ - return self.dict.get(key) or [] - - #: Aliases for WTForms to mimic other multi-dict APIs (Django) - getone = get - getlist = getall - - -class FormsDict(MultiDict): - """ This :class:`MultiDict` subclass is used to store request form data. - Additionally to the normal dict-like item access methods (which return - unmodified data as native strings), this container also supports - attribute-like access to its values. Attributes are automatically de- - or recoded to match :attr:`input_encoding` (default: 'utf8'). Missing - attributes default to an empty string. """ - - #: Encoding used for attribute values. - input_encoding = 'utf8' - #: If true (default), unicode strings are first encoded with `latin1` - #: and then decoded to match :attr:`input_encoding`. - recode_unicode = True - - def _fix(self, s, encoding=None): - if isinstance(s, unicode) and self.recode_unicode: # Python 3 WSGI - return s.encode('latin1').decode(encoding or self.input_encoding) - elif isinstance(s, bytes): # Python 2 WSGI - return s.decode(encoding or self.input_encoding) - else: - return s - - def decode(self, encoding=None): - """ Returns a copy with all keys and values de- or recoded to match - :attr:`input_encoding`. Some libraries (e.g. WTForms) want a - unicode dictionary. """ - copy = FormsDict() - enc = copy.input_encoding = encoding or self.input_encoding - copy.recode_unicode = False - for key, value in self.allitems(): - copy.append(self._fix(key, enc), self._fix(value, enc)) - return copy - - def getunicode(self, name, default=None, encoding=None): - """ Return the value as a unicode string, or the default. """ - try: - return self._fix(self[name], encoding) - except (UnicodeError, KeyError): - return default - - def __getattr__(self, name, default=unicode()): - # Without this guard, pickle generates a cryptic TypeError: - if name.startswith('__') and name.endswith('__'): - return super(FormsDict, self).__getattr__(name) - return self.getunicode(name, default=default) - -class HeaderDict(MultiDict): - """ A case-insensitive version of :class:`MultiDict` that defaults to - replace the old value instead of appending it. """ - - def __init__(self, *a, **ka): - self.dict = {} - if a or ka: self.update(*a, **ka) - - def __contains__(self, key): - return _hkey(key) in self.dict - - def __delitem__(self, key): - del self.dict[_hkey(key)] - - def __getitem__(self, key): - return self.dict[_hkey(key)][-1] - - def __setitem__(self, key, value): - self.dict[_hkey(key)] = [_hval(value)] - - def append(self, key, value): - self.dict.setdefault(_hkey(key), []).append(_hval(value)) - - def replace(self, key, value): - self.dict[_hkey(key)] = [_hval(value)] - - def getall(self, key): - return self.dict.get(_hkey(key)) or [] - - def get(self, key, default=None, index=-1): - return MultiDict.get(self, _hkey(key), default, index) - - def filter(self, names): - for name in (_hkey(n) for n in names): - if name in self.dict: - del self.dict[name] - - -class WSGIHeaderDict(DictMixin): - """ This dict-like class wraps a WSGI environ dict and provides convenient - access to HTTP_* fields. Keys and values are native strings - (2.x bytes or 3.x unicode) and keys are case-insensitive. If the WSGI - environment contains non-native string values, these are de- or encoded - using a lossless 'latin1' character set. - - The API will remain stable even on changes to the relevant PEPs. - Currently PEP 333, 444 and 3333 are supported. (PEP 444 is the only one - that uses non-native strings.) - """ - #: List of keys that do not have a ``HTTP_`` prefix. - cgikeys = ('CONTENT_TYPE', 'CONTENT_LENGTH') - - def __init__(self, environ): - self.environ = environ - - def _ekey(self, key): - """ Translate header field name to CGI/WSGI environ key. """ - key = key.replace('-', '_').upper() - if key in self.cgikeys: - return key - return 'HTTP_' + key - - def raw(self, key, default=None): - """ Return the header value as is (may be bytes or unicode). """ - return self.environ.get(self._ekey(key), default) - - def __getitem__(self, key): - val = self.environ[self._ekey(key)] - if py3k: - if isinstance(val, unicode): - val = val.encode('latin1').decode('utf8') - else: - val = val.decode('utf8') - return val - - def __setitem__(self, key, value): - raise TypeError("%s is read-only." % self.__class__) - - def __delitem__(self, key): - raise TypeError("%s is read-only." % self.__class__) - - def __iter__(self): - for key in self.environ: - if key[:5] == 'HTTP_': - yield _hkey(key[5:]) - elif key in self.cgikeys: - yield _hkey(key) - - def keys(self): - return [x for x in self] - - def __len__(self): - return len(self.keys()) - - def __contains__(self, key): - return self._ekey(key) in self.environ - -_UNSET = object() - -class ConfigDict(dict): - """ A dict-like configuration storage with additional support for - namespaces, validators, meta-data, overlays and more. - - This dict-like class is heavily optimized for read access. All read-only - methods as well as item access should be as fast as the built-in dict. - """ - - __slots__ = ('_meta', '_change_listener', '_overlays', '_virtual_keys', '_source', '__weakref__') - - def __init__(self): - self._meta = {} - self._change_listener = [] - #: Weak references of overlays that need to be kept in sync. - self._overlays = [] - #: Config that is the source for this overlay. - self._source = None - #: Keys of values copied from the source (values we do not own) - self._virtual_keys = set() - - def load_module(self, path, squash=True): - """Load values from a Python module. - - Example modue ``config.py``:: - - DEBUG = True - SQLITE = { - "db": ":memory:" - } - - - >>> c = ConfigDict() - >>> c.load_module('config') - {DEBUG: True, 'SQLITE.DB': 'memory'} - >>> c.load_module("config", False) - {'DEBUG': True, 'SQLITE': {'DB': 'memory'}} - - :param squash: If true (default), dictionary values are assumed to - represent namespaces (see :meth:`load_dict`). - """ - config_obj = load(path) - obj = {key: getattr(config_obj, key) for key in dir(config_obj) - if key.isupper()} - - if squash: - self.load_dict(obj) - else: - self.update(obj) - return self - - def load_config(self, filename, **options): - """ Load values from an ``*.ini`` style config file. - - A configuration file consists of sections, each led by a - ``[section]`` header, followed by key/value entries separated by - either ``=`` or ``:``. Section names and keys are case-insensitive. - Leading and trailing whitespace is removed from keys and values. - Values can be omitted, in which case the key/value delimiter may - also be left out. Values can also span multiple lines, as long as - they are indented deeper than the first line of the value. Commands - are prefixed by ``#`` or ``;`` and may only appear on their own on - an otherwise empty line. - - Both section and key names may contain dots (``.``) as namespace - separators. The actual configuration parameter name is constructed - by joining section name and key name together and converting to - lower case. - - The special sections ``bottle`` and ``ROOT`` refer to the root - namespace and the ``DEFAULT`` section defines default values for all - other sections. - - With Python 3, extended string interpolation is enabled. - - :param filename: The path of a config file, or a list of paths. - :param options: All keyword parameters are passed to the underlying - :class:`python:configparser.ConfigParser` constructor call. - - """ - options.setdefault('allow_no_value', True) - if py3k: - options.setdefault('interpolation', - configparser.ExtendedInterpolation()) - conf = configparser.ConfigParser(**options) - conf.read(filename) - for section in conf.sections(): - for key in conf.options(section): - value = conf.get(section, key) - if section not in ['bottle', 'ROOT']: - key = section + '.' + key - self[key.lower()] = value - return self - - def load_dict(self, source, namespace=''): - """ Load values from a dictionary structure. Nesting can be used to - represent namespaces. - - >>> c = ConfigDict() - >>> c.load_dict({'some': {'namespace': {'key': 'value'} } }) - {'some.namespace.key': 'value'} - """ - for key, value in source.items(): - if isinstance(key, basestring): - nskey = (namespace + '.' + key).strip('.') - if isinstance(value, dict): - self.load_dict(value, namespace=nskey) - else: - self[nskey] = value - else: - raise TypeError('Key has type %r (not a string)' % type(key)) - return self - - def update(self, *a, **ka): - """ If the first parameter is a string, all keys are prefixed with this - namespace. Apart from that it works just as the usual dict.update(). - - >>> c = ConfigDict() - >>> c.update('some.namespace', key='value') - """ - prefix = '' - if a and isinstance(a[0], basestring): - prefix = a[0].strip('.') + '.' - a = a[1:] - for key, value in dict(*a, **ka).items(): - self[prefix + key] = value - - def setdefault(self, key, value): - if key not in self: - self[key] = value - return self[key] - - def __setitem__(self, key, value): - if not isinstance(key, basestring): - raise TypeError('Key has type %r (not a string)' % type(key)) - - self._virtual_keys.discard(key) - - value = self.meta_get(key, 'filter', lambda x: x)(value) - if key in self and self[key] is value: - return - - self._on_change(key, value) - dict.__setitem__(self, key, value) - - for overlay in self._iter_overlays(): - overlay._set_virtual(key, value) - - def __delitem__(self, key): - if key not in self: - raise KeyError(key) - if key in self._virtual_keys: - raise KeyError("Virtual keys cannot be deleted: %s" % key) - - if self._source and key in self._source: - # Not virtual, but present in source -> Restore virtual value - dict.__delitem__(self, key) - self._set_virtual(key, self._source[key]) - else: # not virtual, not present in source. This is OUR value - self._on_change(key, None) - dict.__delitem__(self, key) - for overlay in self._iter_overlays(): - overlay._delete_virtual(key) - - def _set_virtual(self, key, value): - """ Recursively set or update virtual keys. Do nothing if non-virtual - value is present. """ - if key in self and key not in self._virtual_keys: - return # Do nothing for non-virtual keys. - - self._virtual_keys.add(key) - if key in self and self[key] is not value: - self._on_change(key, value) - dict.__setitem__(self, key, value) - for overlay in self._iter_overlays(): - overlay._set_virtual(key, value) - - def _delete_virtual(self, key): - """ Recursively delete virtual entry. Do nothing if key is not virtual. - """ - if key not in self._virtual_keys: - return # Do nothing for non-virtual keys. - - if key in self: - self._on_change(key, None) - dict.__delitem__(self, key) - self._virtual_keys.discard(key) - for overlay in self._iter_overlays(): - overlay._delete_virtual(key) - - def _on_change(self, key, value): - for cb in self._change_listener: - if cb(self, key, value): - return True - - def _add_change_listener(self, func): - self._change_listener.append(func) - return func - - def meta_get(self, key, metafield, default=None): - """ Return the value of a meta field for a key. """ - return self._meta.get(key, {}).get(metafield, default) - - def meta_set(self, key, metafield, value): - """ Set the meta field for a key to a new value. """ - self._meta.setdefault(key, {})[metafield] = value - - def meta_list(self, key): - """ Return an iterable of meta field names defined for a key. """ - return self._meta.get(key, {}).keys() - - def _define(self, key, default=_UNSET, help=_UNSET, validate=_UNSET): - """ (Unstable) Shortcut for plugins to define own config parameters. """ - if default is not _UNSET: - self.setdefault(key, default) - if help is not _UNSET: - self.meta_set(key, 'help', help) - if validate is not _UNSET: - self.meta_set(key, 'validate', validate) - - def _iter_overlays(self): - for ref in self._overlays: - overlay = ref() - if overlay is not None: - yield overlay - - def _make_overlay(self): - """ (Unstable) Create a new overlay that acts like a chained map: Values - missing in the overlay are copied from the source map. Both maps - share the same meta entries. - - Entries that were copied from the source are called 'virtual'. You - can not delete virtual keys, but overwrite them, which turns them - into non-virtual entries. Setting keys on an overlay never affects - its source, but may affect any number of child overlays. - - Other than collections.ChainMap or most other implementations, this - approach does not resolve missing keys on demand, but instead - actively copies all values from the source to the overlay and keeps - track of virtual and non-virtual keys internally. This removes any - lookup-overhead. Read-access is as fast as a build-in dict for both - virtual and non-virtual keys. - - Changes are propagated recursively and depth-first. A failing - on-change handler in an overlay stops the propagation of virtual - values and may result in an partly updated tree. Take extra care - here and make sure that on-change handlers never fail. - - Used by Route.config - """ - # Cleanup dead references - self._overlays[:] = [ref for ref in self._overlays if ref() is not None] - - overlay = ConfigDict() - overlay._meta = self._meta - overlay._source = self - self._overlays.append(weakref.ref(overlay)) - for key in self: - overlay._set_virtual(key, self[key]) - return overlay - - - - -class AppStack(list): - """ A stack-like list. Calling it returns the head of the stack. """ - - def __call__(self): - """ Return the current default application. """ - return self.default - - def push(self, value=None): - """ Add a new :class:`Bottle` instance to the stack """ - if not isinstance(value, Bottle): - value = Bottle() - self.append(value) - return value - new_app = push - - @property - def default(self): - try: - return self[-1] - except IndexError: - return self.push() - - -class WSGIFileWrapper(object): - def __init__(self, fp, buffer_size=1024 * 64): - self.fp, self.buffer_size = fp, buffer_size - for attr in ('fileno', 'close', 'read', 'readlines', 'tell', 'seek'): - if hasattr(fp, attr): setattr(self, attr, getattr(fp, attr)) - - def __iter__(self): - buff, read = self.buffer_size, self.read - while True: - part = read(buff) - if not part: return - yield part - - -class _closeiter(object): - """ This only exists to be able to attach a .close method to iterators that - do not support attribute assignment (most of itertools). """ - - def __init__(self, iterator, close=None): - self.iterator = iterator - self.close_callbacks = makelist(close) - - def __iter__(self): - return iter(self.iterator) - - def close(self): - for func in self.close_callbacks: - func() - - -class ResourceManager(object): - """ This class manages a list of search paths and helps to find and open - application-bound resources (files). - - :param base: default value for :meth:`add_path` calls. - :param opener: callable used to open resources. - :param cachemode: controls which lookups are cached. One of 'all', - 'found' or 'none'. - """ - - def __init__(self, base='./', opener=open, cachemode='all'): - self.opener = opener - self.base = base - self.cachemode = cachemode - - #: A list of search paths. See :meth:`add_path` for details. - self.path = [] - #: A cache for resolved paths. ``res.cache.clear()`` clears the cache. - self.cache = {} - - def add_path(self, path, base=None, index=None, create=False): - """ Add a new path to the list of search paths. Return False if the - path does not exist. - - :param path: The new search path. Relative paths are turned into - an absolute and normalized form. If the path looks like a file - (not ending in `/`), the filename is stripped off. - :param base: Path used to absolutize relative search paths. - Defaults to :attr:`base` which defaults to ``os.getcwd()``. - :param index: Position within the list of search paths. Defaults - to last index (appends to the list). - - The `base` parameter makes it easy to reference files installed - along with a python module or package:: - - res.add_path('./resources/', __file__) - """ - base = os.path.abspath(os.path.dirname(base or self.base)) - path = os.path.abspath(os.path.join(base, os.path.dirname(path))) - path += os.sep - if path in self.path: - self.path.remove(path) - if create and not os.path.isdir(path): - os.makedirs(path) - if index is None: - self.path.append(path) - else: - self.path.insert(index, path) - self.cache.clear() - return os.path.exists(path) - - def __iter__(self): - """ Iterate over all existing files in all registered paths. """ - search = self.path[:] - while search: - path = search.pop() - if not os.path.isdir(path): continue - for name in os.listdir(path): - full = os.path.join(path, name) - if os.path.isdir(full): search.append(full) - else: yield full - - def lookup(self, name): - """ Search for a resource and return an absolute file path, or `None`. - - The :attr:`path` list is searched in order. The first match is - returend. Symlinks are followed. The result is cached to speed up - future lookups. """ - if name not in self.cache or DEBUG: - for path in self.path: - fpath = os.path.join(path, name) - if os.path.isfile(fpath): - if self.cachemode in ('all', 'found'): - self.cache[name] = fpath - return fpath - if self.cachemode == 'all': - self.cache[name] = None - return self.cache[name] - - def open(self, name, mode='r', *args, **kwargs): - """ Find a resource and return a file object, or raise IOError. """ - fname = self.lookup(name) - if not fname: raise IOError("Resource %r not found." % name) - return self.opener(fname, mode=mode, *args, **kwargs) - - -class FileUpload(object): - def __init__(self, fileobj, name, filename, headers=None): - """ Wrapper for file uploads. """ - #: Open file(-like) object (BytesIO buffer or temporary file) - self.file = fileobj - #: Name of the upload form field - self.name = name - #: Raw filename as sent by the client (may contain unsafe characters) - self.raw_filename = filename - #: A :class:`HeaderDict` with additional headers (e.g. content-type) - self.headers = HeaderDict(headers) if headers else HeaderDict() - - content_type = HeaderProperty('Content-Type') - content_length = HeaderProperty('Content-Length', reader=int, default=-1) - - def get_header(self, name, default=None): - """ Return the value of a header within the mulripart part. """ - return self.headers.get(name, default) - - @cached_property - def filename(self): - """ Name of the file on the client file system, but normalized to ensure - file system compatibility. An empty filename is returned as 'empty'. - - Only ASCII letters, digits, dashes, underscores and dots are - allowed in the final filename. Accents are removed, if possible. - Whitespace is replaced by a single dash. Leading or tailing dots - or dashes are removed. The filename is limited to 255 characters. - """ - fname = self.raw_filename - if not isinstance(fname, unicode): - fname = fname.decode('utf8', 'ignore') - fname = normalize('NFKD', fname) - fname = fname.encode('ASCII', 'ignore').decode('ASCII') - fname = os.path.basename(fname.replace('\\', os.path.sep)) - fname = re.sub(r'[^a-zA-Z0-9-_.\s]', '', fname).strip() - fname = re.sub(r'[-\s]+', '-', fname).strip('.-') - return fname[:255] or 'empty' - - def _copy_file(self, fp, chunk_size=2 ** 16): - read, write, offset = self.file.read, fp.write, self.file.tell() - while 1: - buf = read(chunk_size) - if not buf: break - write(buf) - self.file.seek(offset) - - def save(self, destination, overwrite=False, chunk_size=2 ** 16): - """ Save file to disk or copy its content to an open file(-like) object. - If *destination* is a directory, :attr:`filename` is added to the - path. Existing files are not overwritten by default (IOError). - - :param destination: File path, directory or file(-like) object. - :param overwrite: If True, replace existing files. (default: False) - :param chunk_size: Bytes to read at a time. (default: 64kb) - """ - if isinstance(destination, basestring): # Except file-likes here - if os.path.isdir(destination): - destination = os.path.join(destination, self.filename) - if not overwrite and os.path.exists(destination): - raise IOError('File exists.') - with open(destination, 'wb') as fp: - self._copy_file(fp, chunk_size) - else: - self._copy_file(destination, chunk_size) - -############################################################################### -# Application Helper ########################################################### -############################################################################### - - -def abort(code=500, text='Unknown Error.'): - """ Aborts execution and causes a HTTP error. """ - raise HTTPError(code, text) - - -def redirect(url, code=None): - """ Aborts execution and causes a 303 or 302 redirect, depending on - the HTTP protocol version. """ - if not code: - code = 303 if request.get('SERVER_PROTOCOL') == "HTTP/1.1" else 302 - res = response.copy(cls=HTTPResponse) - res.status = code - res.body = "" - res.set_header('Location', urljoin(request.url, url)) - raise res - - -def _file_iter_range(fp, offset, bytes, maxread=1024 * 1024, close=False): - """ Yield chunks from a range in a file, optionally closing it at the end. - No chunk is bigger than maxread. """ - fp.seek(offset) - while bytes > 0: - part = fp.read(min(bytes, maxread)) - if not part: - break - bytes -= len(part) - yield part - if close: - fp.close() - - -def static_file(filename, root, - mimetype=True, - download=False, - charset='UTF-8', - etag=None): - """ Open a file in a safe way and return an instance of :exc:`HTTPResponse` - that can be sent back to the client. - - :param filename: Name or path of the file to send, relative to ``root``. - :param root: Root path for file lookups. Should be an absolute directory - path. - :param mimetype: Provide the content-type header (default: guess from - file extension) - :param download: If True, ask the browser to open a `Save as...` dialog - instead of opening the file with the associated program. You can - specify a custom filename as a string. If not specified, the - original filename is used (default: False). - :param charset: The charset for files with a ``text/*`` mime-type. - (default: UTF-8) - :param etag: Provide a pre-computed ETag header. If set to ``False``, - ETag handling is disabled. (default: auto-generate ETag header) - - While checking user input is always a good idea, this function provides - additional protection against malicious ``filename`` parameters from - breaking out of the ``root`` directory and leaking sensitive information - to an attacker. - - Read-protected files or files outside of the ``root`` directory are - answered with ``403 Access Denied``. Missing files result in a - ``404 Not Found`` response. Conditional requests (``If-Modified-Since``, - ``If-None-Match``) are answered with ``304 Not Modified`` whenever - possible. ``HEAD`` and ``Range`` requests (used by download managers to - check or continue partial downloads) are also handled automatically. - - """ - - root = os.path.join(os.path.abspath(root), '') - filename = os.path.abspath(os.path.join(root, filename.strip('/\\'))) - headers = dict() - - if not filename.startswith(root): - return HTTPError(403, "Access denied.") - if not os.path.exists(filename) or not os.path.isfile(filename): - return HTTPError(404, "File does not exist.") - if not os.access(filename, os.R_OK): - return HTTPError(403, "You do not have permission to access this file.") - - if mimetype is True: - if download and download is not True: - mimetype, encoding = mimetypes.guess_type(download) - else: - mimetype, encoding = mimetypes.guess_type(filename) - if encoding: headers['Content-Encoding'] = encoding - - if mimetype: - if (mimetype[:5] == 'text/' or mimetype == 'application/javascript')\ - and charset and 'charset' not in mimetype: - mimetype += '; charset=%s' % charset - headers['Content-Type'] = mimetype - - if download: - download = os.path.basename(filename if download is True else download) - headers['Content-Disposition'] = 'attachment; filename="%s"' % download - - stats = os.stat(filename) - headers['Content-Length'] = clen = stats.st_size - headers['Last-Modified'] = email.utils.formatdate(stats.st_mtime, - usegmt=True) - headers['Date'] = email.utils.formatdate(time.time(), usegmt=True) - - getenv = request.environ.get - - if etag is None: - etag = '%d:%d:%d:%d:%s' % (stats.st_dev, stats.st_ino, stats.st_mtime, - clen, filename) - etag = hashlib.sha1(tob(etag)).hexdigest() - - if etag: - headers['ETag'] = etag - check = getenv('HTTP_IF_NONE_MATCH') - if check and check == etag: - return HTTPResponse(status=304, **headers) - - ims = getenv('HTTP_IF_MODIFIED_SINCE') - if ims: - ims = parse_date(ims.split(";")[0].strip()) - if ims is not None and ims >= int(stats.st_mtime): - return HTTPResponse(status=304, **headers) - - body = '' if request.method == 'HEAD' else open(filename, 'rb') - - headers["Accept-Ranges"] = "bytes" - range_header = getenv('HTTP_RANGE') - if range_header: - ranges = list(parse_range_header(range_header, clen)) - if not ranges: - return HTTPError(416, "Requested Range Not Satisfiable") - offset, end = ranges[0] - headers["Content-Range"] = "bytes %d-%d/%d" % (offset, end - 1, clen) - headers["Content-Length"] = str(end - offset) - if body: body = _file_iter_range(body, offset, end - offset, close=True) - return HTTPResponse(body, status=206, **headers) - return HTTPResponse(body, **headers) - -############################################################################### -# HTTP Utilities and MISC (TODO) ############################################### -############################################################################### - - -def debug(mode=True): - """ Change the debug level. - There is only one debug level supported at the moment.""" - global DEBUG - if mode: warnings.simplefilter('default') - DEBUG = bool(mode) - - -def http_date(value): - if isinstance(value, (datedate, datetime)): - value = value.utctimetuple() - elif isinstance(value, (int, float)): - value = time.gmtime(value) - if not isinstance(value, basestring): - value = time.strftime("%a, %d %b %Y %H:%M:%S GMT", value) - return value - - -def parse_date(ims): - """ Parse rfc1123, rfc850 and asctime timestamps and return UTC epoch. """ - try: - ts = email.utils.parsedate_tz(ims) - return time.mktime(ts[:8] + (0, )) - (ts[9] or 0) - time.timezone - except (TypeError, ValueError, IndexError, OverflowError): - return None - - -def parse_auth(header): - """ Parse rfc2617 HTTP authentication header string (basic) and return (user,pass) tuple or None""" - try: - method, data = header.split(None, 1) - if method.lower() == 'basic': - user, pwd = touni(base64.b64decode(tob(data))).split(':', 1) - return user, pwd - except (KeyError, ValueError): - return None - - -def parse_range_header(header, maxlen=0): - """ Yield (start, end) ranges parsed from a HTTP Range header. Skip - unsatisfiable ranges. The end index is non-inclusive.""" - if not header or header[:6] != 'bytes=': return - ranges = [r.split('-', 1) for r in header[6:].split(',') if '-' in r] - for start, end in ranges: - try: - if not start: # bytes=-100 -> last 100 bytes - start, end = max(0, maxlen - int(end)), maxlen - elif not end: # bytes=100- -> all but the first 99 bytes - start, end = int(start), maxlen - else: # bytes=100-200 -> bytes 100-200 (inclusive) - start, end = int(start), min(int(end) + 1, maxlen) - if 0 <= start < end <= maxlen: - yield start, end - except ValueError: - pass - - -#: Header tokenizer used by _parse_http_header() -_hsplit = re.compile('(?:(?:"((?:[^"\\\\]+|\\\\.)*)")|([^;,=]+))([;,=]?)').findall - -def _parse_http_header(h): - """ Parses a typical multi-valued and parametrised HTTP header (e.g. Accept headers) and returns a list of values - and parameters. For non-standard or broken input, this implementation may return partial results. - :param h: A header string (e.g. ``text/html,text/plain;q=0.9,*/*;q=0.8``) - :return: List of (value, params) tuples. The second element is a (possibly empty) dict. - """ - values = [] - if '"' not in h: # INFO: Fast path without regexp (~2x faster) - for value in h.split(','): - parts = value.split(';') - values.append((parts[0].strip(), {})) - for attr in parts[1:]: - name, value = attr.split('=', 1) - values[-1][1][name.strip()] = value.strip() - else: - lop, key, attrs = ',', None, {} - for quoted, plain, tok in _hsplit(h): - value = plain.strip() if plain else quoted.replace('\\"', '"') - if lop == ',': - attrs = {} - values.append((value, attrs)) - elif lop == ';': - if tok == '=': - key = value - else: - attrs[value] = '' - elif lop == '=' and key: - attrs[key] = value - key = None - lop = tok - return values - - -def _parse_qsl(qs): - r = [] - for pair in qs.replace(';', '&').split('&'): - if not pair: continue - nv = pair.split('=', 1) - if len(nv) != 2: nv.append('') - key = urlunquote(nv[0].replace('+', ' ')) - value = urlunquote(nv[1].replace('+', ' ')) - r.append((key, value)) - return r - - -def _lscmp(a, b): - """ Compares two strings in a cryptographically safe way: - Runtime is not affected by length of common prefix. """ - return not sum(0 if x == y else 1 - for x, y in zip(a, b)) and len(a) == len(b) - - -def cookie_encode(data, key, digestmod=None): - """ Encode and sign a pickle-able object. Return a (byte) string """ - depr(0, 13, "cookie_encode() will be removed soon.", - "Do not use this API directly.") - digestmod = digestmod or hashlib.sha256 - msg = base64.b64encode(pickle.dumps(data, -1)) - sig = base64.b64encode(hmac.new(tob(key), msg, digestmod=digestmod).digest()) - return tob('!') + sig + tob('?') + msg - - -def cookie_decode(data, key, digestmod=None): - """ Verify and decode an encoded string. Return an object or None.""" - depr(0, 13, "cookie_decode() will be removed soon.", - "Do not use this API directly.") - data = tob(data) - if cookie_is_encoded(data): - sig, msg = data.split(tob('?'), 1) - digestmod = digestmod or hashlib.sha256 - hashed = hmac.new(tob(key), msg, digestmod=digestmod).digest() - if _lscmp(sig[1:], base64.b64encode(hashed)): - return pickle.loads(base64.b64decode(msg)) - return None - - -def cookie_is_encoded(data): - """ Return True if the argument looks like a encoded cookie.""" - depr(0, 13, "cookie_is_encoded() will be removed soon.", - "Do not use this API directly.") - return bool(data.startswith(tob('!')) and tob('?') in data) - - -def html_escape(string): - """ Escape HTML special characters ``&<>`` and quotes ``'"``. """ - return string.replace('&', '&').replace('<', '<').replace('>', '>')\ - .replace('"', '"').replace("'", ''') - - -def html_quote(string): - """ Escape and quote a string to be used as an HTTP attribute.""" - return '"%s"' % html_escape(string).replace('\n', ' ')\ - .replace('\r', ' ').replace('\t', ' ') - - -def yieldroutes(func): - """ Return a generator for routes that match the signature (name, args) - of the func parameter. This may yield more than one route if the function - takes optional keyword arguments. The output is best described by example:: - - a() -> '/a' - b(x, y) -> '/b//' - c(x, y=5) -> '/c/' and '/c//' - d(x=5, y=6) -> '/d' and '/d/' and '/d//' - """ - path = '/' + func.__name__.replace('__', '/').lstrip('/') - spec = getargspec(func) - argc = len(spec[0]) - len(spec[3] or []) - path += ('/<%s>' * argc) % tuple(spec[0][:argc]) - yield path - for arg in spec[0][argc:]: - path += '/<%s>' % arg - yield path - - -def path_shift(script_name, path_info, shift=1): - """ Shift path fragments from PATH_INFO to SCRIPT_NAME and vice versa. - - :return: The modified paths. - :param script_name: The SCRIPT_NAME path. - :param script_name: The PATH_INFO path. - :param shift: The number of path fragments to shift. May be negative to - change the shift direction. (default: 1) - """ - if shift == 0: return script_name, path_info - pathlist = path_info.strip('/').split('/') - scriptlist = script_name.strip('/').split('/') - if pathlist and pathlist[0] == '': pathlist = [] - if scriptlist and scriptlist[0] == '': scriptlist = [] - if 0 < shift <= len(pathlist): - moved = pathlist[:shift] - scriptlist = scriptlist + moved - pathlist = pathlist[shift:] - elif 0 > shift >= -len(scriptlist): - moved = scriptlist[shift:] - pathlist = moved + pathlist - scriptlist = scriptlist[:shift] - else: - empty = 'SCRIPT_NAME' if shift < 0 else 'PATH_INFO' - raise AssertionError("Cannot shift. Nothing left from %s" % empty) - new_script_name = '/' + '/'.join(scriptlist) - new_path_info = '/' + '/'.join(pathlist) - if path_info.endswith('/') and pathlist: new_path_info += '/' - return new_script_name, new_path_info - - -def auth_basic(check, realm="private", text="Access denied"): - """ Callback decorator to require HTTP auth (basic). - TODO: Add route(check_auth=...) parameter. """ - - def decorator(func): - - @functools.wraps(func) - def wrapper(*a, **ka): - user, password = request.auth or (None, None) - if user is None or not check(user, password): - err = HTTPError(401, text) - err.add_header('WWW-Authenticate', 'Basic realm="%s"' % realm) - return err - return func(*a, **ka) - - return wrapper - - return decorator - -# Shortcuts for common Bottle methods. -# They all refer to the current default application. - - -def make_default_app_wrapper(name): - """ Return a callable that relays calls to the current default app. """ - - @functools.wraps(getattr(Bottle, name)) - def wrapper(*a, **ka): - return getattr(app(), name)(*a, **ka) - - return wrapper - - -route = make_default_app_wrapper('route') -get = make_default_app_wrapper('get') -post = make_default_app_wrapper('post') -put = make_default_app_wrapper('put') -delete = make_default_app_wrapper('delete') -patch = make_default_app_wrapper('patch') -error = make_default_app_wrapper('error') -mount = make_default_app_wrapper('mount') -hook = make_default_app_wrapper('hook') -install = make_default_app_wrapper('install') -uninstall = make_default_app_wrapper('uninstall') -url = make_default_app_wrapper('get_url') - -############################################################################### -# Server Adapter ############################################################### -############################################################################### - -# Before you edit or add a server adapter, please read: -# - https://github.com/bottlepy/bottle/pull/647#issuecomment-60152870 -# - https://github.com/bottlepy/bottle/pull/865#issuecomment-242795341 - -class ServerAdapter(object): - quiet = False - - def __init__(self, host='127.0.0.1', port=8080, **options): - self.options = options - self.host = host - self.port = int(port) - - def run(self, handler): # pragma: no cover - pass - - def __repr__(self): - args = ', '.join(['%s=%s' % (k, repr(v)) - for k, v in self.options.items()]) - return "%s(%s)" % (self.__class__.__name__, args) - - -class CGIServer(ServerAdapter): - quiet = True - - def run(self, handler): # pragma: no cover - from wsgiref.handlers import CGIHandler - - def fixed_environ(environ, start_response): - environ.setdefault('PATH_INFO', '') - return handler(environ, start_response) - - CGIHandler().run(fixed_environ) - - -class FlupFCGIServer(ServerAdapter): - def run(self, handler): # pragma: no cover - import flup.server.fcgi - self.options.setdefault('bindAddress', (self.host, self.port)) - flup.server.fcgi.WSGIServer(handler, **self.options).run() - - -class WSGIRefServer(ServerAdapter): - def run(self, app): # pragma: no cover - from wsgiref.simple_server import make_server - from wsgiref.simple_server import WSGIRequestHandler, WSGIServer - import socket - - class FixedHandler(WSGIRequestHandler): - def address_string(self): # Prevent reverse DNS lookups please. - return self.client_address[0] - - def log_request(*args, **kw): - if not self.quiet: - return WSGIRequestHandler.log_request(*args, **kw) - - handler_cls = self.options.get('handler_class', FixedHandler) - server_cls = self.options.get('server_class', WSGIServer) - - if ':' in self.host: # Fix wsgiref for IPv6 addresses. - if getattr(server_cls, 'address_family') == socket.AF_INET: - - class server_cls(server_cls): - address_family = socket.AF_INET6 - - self.srv = make_server(self.host, self.port, app, server_cls, - handler_cls) - self.port = self.srv.server_port # update port actual port (0 means random) - try: - self.srv.serve_forever() - except KeyboardInterrupt: - self.srv.server_close() # Prevent ResourceWarning: unclosed socket - raise - - -class CherryPyServer(ServerAdapter): - def run(self, handler): # pragma: no cover - depr(0, 13, "The wsgi server part of cherrypy was split into a new " - "project called 'cheroot'.", "Use the 'cheroot' server " - "adapter instead of cherrypy.") - from cherrypy import wsgiserver # This will fail for CherryPy >= 9 - - self.options['bind_addr'] = (self.host, self.port) - self.options['wsgi_app'] = handler - - certfile = self.options.get('certfile') - if certfile: - del self.options['certfile'] - keyfile = self.options.get('keyfile') - if keyfile: - del self.options['keyfile'] - - server = wsgiserver.CherryPyWSGIServer(**self.options) - if certfile: - server.ssl_certificate = certfile - if keyfile: - server.ssl_private_key = keyfile - - try: - server.start() - finally: - server.stop() - - -class CherootServer(ServerAdapter): - def run(self, handler): # pragma: no cover - from cheroot import wsgi - from cheroot.ssl import builtin - self.options['bind_addr'] = (self.host, self.port) - self.options['wsgi_app'] = handler - certfile = self.options.pop('certfile', None) - keyfile = self.options.pop('keyfile', None) - chainfile = self.options.pop('chainfile', None) - server = wsgi.Server(**self.options) - if certfile and keyfile: - server.ssl_adapter = builtin.BuiltinSSLAdapter( - certfile, keyfile, chainfile) - try: - server.start() - finally: - server.stop() - - -class WaitressServer(ServerAdapter): - def run(self, handler): - from waitress import serve - serve(handler, host=self.host, port=self.port, _quiet=self.quiet, **self.options) - - -class PasteServer(ServerAdapter): - def run(self, handler): # pragma: no cover - from paste import httpserver - from paste.translogger import TransLogger - handler = TransLogger(handler, setup_console_handler=(not self.quiet)) - httpserver.serve(handler, - host=self.host, - port=str(self.port), **self.options) - - -class MeinheldServer(ServerAdapter): - def run(self, handler): - from meinheld import server - server.listen((self.host, self.port)) - server.run(handler) - - -class FapwsServer(ServerAdapter): - """ Extremely fast webserver using libev. See http://www.fapws.org/ """ - - def run(self, handler): # pragma: no cover - import fapws._evwsgi as evwsgi - from fapws import base, config - port = self.port - if float(config.SERVER_IDENT[-2:]) > 0.4: - # fapws3 silently changed its API in 0.5 - port = str(port) - evwsgi.start(self.host, port) - # fapws3 never releases the GIL. Complain upstream. I tried. No luck. - if 'BOTTLE_CHILD' in os.environ and not self.quiet: - _stderr("WARNING: Auto-reloading does not work with Fapws3.\n") - _stderr(" (Fapws3 breaks python thread support)\n") - evwsgi.set_base_module(base) - - def app(environ, start_response): - environ['wsgi.multiprocess'] = False - return handler(environ, start_response) - - evwsgi.wsgi_cb(('', app)) - evwsgi.run() - - -class TornadoServer(ServerAdapter): - """ The super hyped asynchronous server by facebook. Untested. """ - - def run(self, handler): # pragma: no cover - import tornado.wsgi, tornado.httpserver, tornado.ioloop - container = tornado.wsgi.WSGIContainer(handler) - server = tornado.httpserver.HTTPServer(container) - server.listen(port=self.port, address=self.host) - tornado.ioloop.IOLoop.instance().start() - - -class AppEngineServer(ServerAdapter): - """ Adapter for Google App Engine. """ - quiet = True - - def run(self, handler): - depr(0, 13, "AppEngineServer no longer required", - "Configure your application directly in your app.yaml") - from google.appengine.ext.webapp import util - # A main() function in the handler script enables 'App Caching'. - # Lets makes sure it is there. This _really_ improves performance. - module = sys.modules.get('__main__') - if module and not hasattr(module, 'main'): - module.main = lambda: util.run_wsgi_app(handler) - util.run_wsgi_app(handler) - - -class TwistedServer(ServerAdapter): - """ Untested. """ - - def run(self, handler): - from twisted.web import server, wsgi - from twisted.python.threadpool import ThreadPool - from twisted.internet import reactor - thread_pool = ThreadPool() - thread_pool.start() - reactor.addSystemEventTrigger('after', 'shutdown', thread_pool.stop) - factory = server.Site(wsgi.WSGIResource(reactor, thread_pool, handler)) - reactor.listenTCP(self.port, factory, interface=self.host) - if not reactor.running: - reactor.run() - - -class DieselServer(ServerAdapter): - """ Untested. """ - - def run(self, handler): - from diesel.protocols.wsgi import WSGIApplication - app = WSGIApplication(handler, port=self.port) - app.run() - - -class GeventServer(ServerAdapter): - """ Untested. Options: - - * See gevent.wsgi.WSGIServer() documentation for more options. - """ - - def run(self, handler): - from gevent import pywsgi, local - if not isinstance(threading.local(), local.local): - msg = "Bottle requires gevent.monkey.patch_all() (before import)" - raise RuntimeError(msg) - if self.quiet: - self.options['log'] = None - address = (self.host, self.port) - server = pywsgi.WSGIServer(address, handler, **self.options) - if 'BOTTLE_CHILD' in os.environ: - import signal - signal.signal(signal.SIGINT, lambda s, f: server.stop()) - server.serve_forever() - - -class GunicornServer(ServerAdapter): - """ Untested. See http://gunicorn.org/configure.html for options. """ - - def run(self, handler): - from gunicorn.app.base import Application - - config = {'bind': "%s:%d" % (self.host, int(self.port))} - config.update(self.options) - - class GunicornApplication(Application): - def init(self, parser, opts, args): - return config - - def load(self): - return handler - - GunicornApplication().run() - - -class EventletServer(ServerAdapter): - """ Untested. Options: - - * `backlog` adjust the eventlet backlog parameter which is the maximum - number of queued connections. Should be at least 1; the maximum - value is system-dependent. - * `family`: (default is 2) socket family, optional. See socket - documentation for available families. - """ - - def run(self, handler): - from eventlet import wsgi, listen, patcher - if not patcher.is_monkey_patched(os): - msg = "Bottle requires eventlet.monkey_patch() (before import)" - raise RuntimeError(msg) - socket_args = {} - for arg in ('backlog', 'family'): - try: - socket_args[arg] = self.options.pop(arg) - except KeyError: - pass - address = (self.host, self.port) - try: - wsgi.server(listen(address, **socket_args), handler, - log_output=(not self.quiet)) - except TypeError: - # Fallback, if we have old version of eventlet - wsgi.server(listen(address), handler) - - -class RocketServer(ServerAdapter): - """ Untested. """ - - def run(self, handler): - from rocket import Rocket - server = Rocket((self.host, self.port), 'wsgi', {'wsgi_app': handler}) - server.start() - - -class BjoernServer(ServerAdapter): - """ Fast server written in C: https://github.com/jonashaag/bjoern """ - - def run(self, handler): - from bjoern import run - run(handler, self.host, self.port) - -class AsyncioServerAdapter(ServerAdapter): - """ Extend ServerAdapter for adding custom event loop """ - def get_event_loop(self): - pass - -class AiohttpServer(AsyncioServerAdapter): - """ Untested. - aiohttp - https://pypi.python.org/pypi/aiohttp/ - """ - - def get_event_loop(self): - import asyncio - return asyncio.new_event_loop() - - def run(self, handler): - import asyncio - from aiohttp.wsgi import WSGIServerHttpProtocol - self.loop = self.get_event_loop() - asyncio.set_event_loop(self.loop) - - protocol_factory = lambda: WSGIServerHttpProtocol( - handler, - readpayload=True, - debug=(not self.quiet)) - self.loop.run_until_complete(self.loop.create_server(protocol_factory, - self.host, - self.port)) - - if 'BOTTLE_CHILD' in os.environ: - import signal - signal.signal(signal.SIGINT, lambda s, f: self.loop.stop()) - - try: - self.loop.run_forever() - except KeyboardInterrupt: - self.loop.stop() - -class AiohttpUVLoopServer(AiohttpServer): - """uvloop - https://github.com/MagicStack/uvloop - """ - def get_event_loop(self): - import uvloop - return uvloop.new_event_loop() - -class AutoServer(ServerAdapter): - """ Untested. """ - adapters = [WaitressServer, PasteServer, TwistedServer, CherryPyServer, - CherootServer, WSGIRefServer] - - def run(self, handler): - for sa in self.adapters: - try: - return sa(self.host, self.port, **self.options).run(handler) - except ImportError: - pass - - -server_names = { - 'cgi': CGIServer, - 'flup': FlupFCGIServer, - 'wsgiref': WSGIRefServer, - 'waitress': WaitressServer, - 'cherrypy': CherryPyServer, - 'cheroot': CherootServer, - 'paste': PasteServer, - 'fapws3': FapwsServer, - 'tornado': TornadoServer, - 'gae': AppEngineServer, - 'twisted': TwistedServer, - 'diesel': DieselServer, - 'meinheld': MeinheldServer, - 'gunicorn': GunicornServer, - 'eventlet': EventletServer, - 'gevent': GeventServer, - 'rocket': RocketServer, - 'bjoern': BjoernServer, - 'aiohttp': AiohttpServer, - 'uvloop': AiohttpUVLoopServer, - 'auto': AutoServer, -} - -############################################################################### -# Application Control ########################################################## -############################################################################### - - -def load(target, **namespace): - """ Import a module or fetch an object from a module. - - * ``package.module`` returns `module` as a module object. - * ``pack.mod:name`` returns the module variable `name` from `pack.mod`. - * ``pack.mod:func()`` calls `pack.mod.func()` and returns the result. - - The last form accepts not only function calls, but any type of - expression. Keyword arguments passed to this function are available as - local variables. Example: ``import_string('re:compile(x)', x='[a-z]')`` - """ - module, target = target.split(":", 1) if ':' in target else (target, None) - if module not in sys.modules: __import__(module) - if not target: return sys.modules[module] - if target.isalnum(): return getattr(sys.modules[module], target) - package_name = module.split('.')[0] - namespace[package_name] = sys.modules[package_name] - return eval('%s.%s' % (module, target), namespace) - - -def load_app(target): - """ Load a bottle application from a module and make sure that the import - does not affect the current default application, but returns a separate - application object. See :func:`load` for the target parameter. """ - global NORUN - NORUN, nr_old = True, NORUN - tmp = default_app.push() # Create a new "default application" - try: - rv = load(target) # Import the target module - return rv if callable(rv) else tmp - finally: - default_app.remove(tmp) # Remove the temporary added default application - NORUN = nr_old - - -_debug = debug - - -def run(app=None, - server='wsgiref', - host='127.0.0.1', - port=8080, - interval=1, - reloader=False, - quiet=False, - plugins=None, - debug=None, - config=None, **kargs): - """ Start a server instance. This method blocks until the server terminates. - - :param app: WSGI application or target string supported by - :func:`load_app`. (default: :func:`default_app`) - :param server: Server adapter to use. See :data:`server_names` keys - for valid names or pass a :class:`ServerAdapter` subclass. - (default: `wsgiref`) - :param host: Server address to bind to. Pass ``0.0.0.0`` to listens on - all interfaces including the external one. (default: 127.0.0.1) - :param port: Server port to bind to. Values below 1024 require root - privileges. (default: 8080) - :param reloader: Start auto-reloading server? (default: False) - :param interval: Auto-reloader interval in seconds (default: 1) - :param quiet: Suppress output to stdout and stderr? (default: False) - :param options: Options passed to the server adapter. - """ - if NORUN: return - if reloader and not os.environ.get('BOTTLE_CHILD'): - import subprocess - lockfile = None - try: - fd, lockfile = tempfile.mkstemp(prefix='bottle.', suffix='.lock') - os.close(fd) # We only need this file to exist. We never write to it - while os.path.exists(lockfile): - args = [sys.executable] + sys.argv - environ = os.environ.copy() - environ['BOTTLE_CHILD'] = 'true' - environ['BOTTLE_LOCKFILE'] = lockfile - p = subprocess.Popen(args, env=environ) - while p.poll() is None: # Busy wait... - os.utime(lockfile, None) # I am alive! - time.sleep(interval) - if p.poll() != 3: - if os.path.exists(lockfile): os.unlink(lockfile) - sys.exit(p.poll()) - except KeyboardInterrupt: - pass - finally: - if os.path.exists(lockfile): - os.unlink(lockfile) - return - - try: - if debug is not None: _debug(debug) - app = app or default_app() - if isinstance(app, basestring): - app = load_app(app) - if not callable(app): - raise ValueError("Application is not callable: %r" % app) - - for plugin in plugins or []: - if isinstance(plugin, basestring): - plugin = load(plugin) - app.install(plugin) - - if config: - app.config.update(config) - - if server in server_names: - server = server_names.get(server) - if isinstance(server, basestring): - server = load(server) - if isinstance(server, type): - server = server(host=host, port=port, **kargs) - if not isinstance(server, ServerAdapter): - raise ValueError("Unknown or unsupported server: %r" % server) - - server.quiet = server.quiet or quiet - if not server.quiet: - _stderr("Bottle v%s server starting up (using %s)...\n" % - (__version__, repr(server))) - _stderr("Listening on http://%s:%d/\n" % - (server.host, server.port)) - _stderr("Hit Ctrl-C to quit.\n\n") - - if reloader: - lockfile = os.environ.get('BOTTLE_LOCKFILE') - bgcheck = FileCheckerThread(lockfile, interval) - with bgcheck: - server.run(app) - if bgcheck.status == 'reload': - sys.exit(3) - else: - server.run(app) - except KeyboardInterrupt: - pass - except (SystemExit, MemoryError): - raise - except: - if not reloader: raise - if not getattr(server, 'quiet', quiet): - print_exc() - time.sleep(interval) - sys.exit(3) - - -class FileCheckerThread(threading.Thread): - """ Interrupt main-thread as soon as a changed module file is detected, - the lockfile gets deleted or gets too old. """ - - def __init__(self, lockfile, interval): - threading.Thread.__init__(self) - self.daemon = True - self.lockfile, self.interval = lockfile, interval - #: Is one of 'reload', 'error' or 'exit' - self.status = None - - def run(self): - exists = os.path.exists - mtime = lambda p: os.stat(p).st_mtime - files = dict() - - for module in list(sys.modules.values()): - path = getattr(module, '__file__', '') or '' - if path[-4:] in ('.pyo', '.pyc'): path = path[:-1] - if path and exists(path): files[path] = mtime(path) - - while not self.status: - if not exists(self.lockfile)\ - or mtime(self.lockfile) < time.time() - self.interval - 5: - self.status = 'error' - thread.interrupt_main() - for path, lmtime in list(files.items()): - if not exists(path) or mtime(path) > lmtime: - self.status = 'reload' - thread.interrupt_main() - break - time.sleep(self.interval) - - def __enter__(self): - self.start() - - def __exit__(self, exc_type, *_): - if not self.status: self.status = 'exit' # silent exit - self.join() - return exc_type is not None and issubclass(exc_type, KeyboardInterrupt) - -############################################################################### -# Template Adapters ############################################################ -############################################################################### - - -class TemplateError(BottleException): - pass - - -class BaseTemplate(object): - """ Base class and minimal API for template adapters """ - extensions = ['tpl', 'html', 'thtml', 'stpl'] - settings = {} #used in prepare() - defaults = {} #used in render() - - def __init__(self, - source=None, - name=None, - lookup=None, - encoding='utf8', **settings): - """ Create a new template. - If the source parameter (str or buffer) is missing, the name argument - is used to guess a template filename. Subclasses can assume that - self.source and/or self.filename are set. Both are strings. - The lookup, encoding and settings parameters are stored as instance - variables. - The lookup parameter stores a list containing directory paths. - The encoding parameter should be used to decode byte strings or files. - The settings parameter contains a dict for engine-specific settings. - """ - self.name = name - self.source = source.read() if hasattr(source, 'read') else source - self.filename = source.filename if hasattr(source, 'filename') else None - self.lookup = [os.path.abspath(x) for x in lookup] if lookup else [] - self.encoding = encoding - self.settings = self.settings.copy() # Copy from class variable - self.settings.update(settings) # Apply - if not self.source and self.name: - self.filename = self.search(self.name, self.lookup) - if not self.filename: - raise TemplateError('Template %s not found.' % repr(name)) - if not self.source and not self.filename: - raise TemplateError('No template specified.') - self.prepare(**self.settings) - - @classmethod - def search(cls, name, lookup=None): - """ Search name in all directories specified in lookup. - First without, then with common extensions. Return first hit. """ - if not lookup: - raise depr(0, 12, "Empty template lookup path.", "Configure a template lookup path.") - - if os.path.isabs(name): - raise depr(0, 12, "Use of absolute path for template name.", - "Refer to templates with names or paths relative to the lookup path.") - - for spath in lookup: - spath = os.path.abspath(spath) + os.sep - fname = os.path.abspath(os.path.join(spath, name)) - if not fname.startswith(spath): continue - if os.path.isfile(fname): return fname - for ext in cls.extensions: - if os.path.isfile('%s.%s' % (fname, ext)): - return '%s.%s' % (fname, ext) - - @classmethod - def global_config(cls, key, *args): - """ This reads or sets the global settings stored in class.settings. """ - if args: - cls.settings = cls.settings.copy() # Make settings local to class - cls.settings[key] = args[0] - else: - return cls.settings[key] - - def prepare(self, **options): - """ Run preparations (parsing, caching, ...). - It should be possible to call this again to refresh a template or to - update settings. - """ - raise NotImplementedError - - def render(self, *args, **kwargs): - """ Render the template with the specified local variables and return - a single byte or unicode string. If it is a byte string, the encoding - must match self.encoding. This method must be thread-safe! - Local variables may be provided in dictionaries (args) - or directly, as keywords (kwargs). - """ - raise NotImplementedError - - -class MakoTemplate(BaseTemplate): - def prepare(self, **options): - from mako.template import Template - from mako.lookup import TemplateLookup - options.update({'input_encoding': self.encoding}) - options.setdefault('format_exceptions', bool(DEBUG)) - lookup = TemplateLookup(directories=self.lookup, **options) - if self.source: - self.tpl = Template(self.source, lookup=lookup, **options) - else: - self.tpl = Template(uri=self.name, - filename=self.filename, - lookup=lookup, **options) - - def render(self, *args, **kwargs): - for dictarg in args: - kwargs.update(dictarg) - _defaults = self.defaults.copy() - _defaults.update(kwargs) - return self.tpl.render(**_defaults) - - -class CheetahTemplate(BaseTemplate): - def prepare(self, **options): - from Cheetah.Template import Template - self.context = threading.local() - self.context.vars = {} - options['searchList'] = [self.context.vars] - if self.source: - self.tpl = Template(source=self.source, **options) - else: - self.tpl = Template(file=self.filename, **options) - - def render(self, *args, **kwargs): - for dictarg in args: - kwargs.update(dictarg) - self.context.vars.update(self.defaults) - self.context.vars.update(kwargs) - out = str(self.tpl) - self.context.vars.clear() - return out - - -class Jinja2Template(BaseTemplate): - def prepare(self, filters=None, tests=None, globals={}, **kwargs): - from jinja2 import Environment, FunctionLoader - self.env = Environment(loader=FunctionLoader(self.loader), **kwargs) - if filters: self.env.filters.update(filters) - if tests: self.env.tests.update(tests) - if globals: self.env.globals.update(globals) - if self.source: - self.tpl = self.env.from_string(self.source) - else: - self.tpl = self.env.get_template(self.name) - - def render(self, *args, **kwargs): - for dictarg in args: - kwargs.update(dictarg) - _defaults = self.defaults.copy() - _defaults.update(kwargs) - return self.tpl.render(**_defaults) - - def loader(self, name): - if name == self.filename: - fname = name - else: - fname = self.search(name, self.lookup) - if not fname: return - with open(fname, "rb") as f: - return (f.read().decode(self.encoding), fname, lambda: False) - - -class SimpleTemplate(BaseTemplate): - def prepare(self, - escape_func=html_escape, - noescape=False, - syntax=None, **ka): - self.cache = {} - enc = self.encoding - self._str = lambda x: touni(x, enc) - self._escape = lambda x: escape_func(touni(x, enc)) - self.syntax = syntax - if noescape: - self._str, self._escape = self._escape, self._str - - @cached_property - def co(self): - return compile(self.code, self.filename or '', 'exec') - - @cached_property - def code(self): - source = self.source - if not source: - with open(self.filename, 'rb') as f: - source = f.read() - try: - source, encoding = touni(source), 'utf8' - except UnicodeError: - raise depr(0, 11, 'Unsupported template encodings.', 'Use utf-8 for templates.') - parser = StplParser(source, encoding=encoding, syntax=self.syntax) - code = parser.translate() - self.encoding = parser.encoding - return code - - def _rebase(self, _env, _name=None, **kwargs): - _env['_rebase'] = (_name, kwargs) - - def _include(self, _env, _name=None, **kwargs): - env = _env.copy() - env.update(kwargs) - if _name not in self.cache: - self.cache[_name] = self.__class__(name=_name, lookup=self.lookup, syntax=self.syntax) - return self.cache[_name].execute(env['_stdout'], env) - - def execute(self, _stdout, kwargs): - env = self.defaults.copy() - env.update(kwargs) - env.update({ - '_stdout': _stdout, - '_printlist': _stdout.extend, - 'include': functools.partial(self._include, env), - 'rebase': functools.partial(self._rebase, env), - '_rebase': None, - '_str': self._str, - '_escape': self._escape, - 'get': env.get, - 'setdefault': env.setdefault, - 'defined': env.__contains__ - }) - exec(self.co, env) - if env.get('_rebase'): - subtpl, rargs = env.pop('_rebase') - rargs['base'] = ''.join(_stdout) #copy stdout - del _stdout[:] # clear stdout - return self._include(env, subtpl, **rargs) - return env - - def render(self, *args, **kwargs): - """ Render the template using keyword arguments as local variables. """ - env = {} - stdout = [] - for dictarg in args: - env.update(dictarg) - env.update(kwargs) - self.execute(stdout, env) - return ''.join(stdout) - - -class StplSyntaxError(TemplateError): - pass - - -class StplParser(object): - """ Parser for stpl templates. """ - _re_cache = {} #: Cache for compiled re patterns - - # This huge pile of voodoo magic splits python code into 8 different tokens. - # We use the verbose (?x) regex mode to make this more manageable - - _re_tok = r'''( - [urbURB]* - (?: ''(?!') - |""(?!") - |'{6} - |"{6} - |'(?:[^\\']|\\.)+?' - |"(?:[^\\"]|\\.)+?" - |'{3}(?:[^\\]|\\.|\n)+?'{3} - |"{3}(?:[^\\]|\\.|\n)+?"{3} - ) - )''' - - _re_inl = _re_tok.replace(r'|\n', '') # We re-use this string pattern later - - _re_tok += r''' - # 2: Comments (until end of line, but not the newline itself) - |(\#.*) - - # 3: Open and close (4) grouping tokens - |([\[\{\(]) - |([\]\}\)]) - - # 5,6: Keywords that start or continue a python block (only start of line) - |^([\ \t]*(?:if|for|while|with|try|def|class)\b) - |^([\ \t]*(?:elif|else|except|finally)\b) - - # 7: Our special 'end' keyword (but only if it stands alone) - |((?:^|;)[\ \t]*end[\ \t]*(?=(?:%(block_close)s[\ \t]*)?\r?$|;|\#)) - - # 8: A customizable end-of-code-block template token (only end of line) - |(%(block_close)s[\ \t]*(?=\r?$)) - - # 9: And finally, a single newline. The 10th token is 'everything else' - |(\r?\n) - ''' - - # Match the start tokens of code areas in a template - _re_split = r'''(?m)^[ \t]*(\\?)((%(line_start)s)|(%(block_start)s))''' - # Match inline statements (may contain python strings) - _re_inl = r'''%%(inline_start)s((?:%s|[^'"\n]+?)*?)%%(inline_end)s''' % _re_inl - - # add the flag in front of the regexp to avoid Deprecation warning (see Issue #949) - # verbose and dot-matches-newline mode - _re_tok = '(?mx)' + _re_tok - _re_inl = '(?mx)' + _re_inl - - - default_syntax = '<% %> % {{ }}' - - def __init__(self, source, syntax=None, encoding='utf8'): - self.source, self.encoding = touni(source, encoding), encoding - self.set_syntax(syntax or self.default_syntax) - self.code_buffer, self.text_buffer = [], [] - self.lineno, self.offset = 1, 0 - self.indent, self.indent_mod = 0, 0 - self.paren_depth = 0 - - def get_syntax(self): - """ Tokens as a space separated string (default: <% %> % {{ }}) """ - return self._syntax - - def set_syntax(self, syntax): - self._syntax = syntax - self._tokens = syntax.split() - if syntax not in self._re_cache: - names = 'block_start block_close line_start inline_start inline_end' - etokens = map(re.escape, self._tokens) - pattern_vars = dict(zip(names.split(), etokens)) - patterns = (self._re_split, self._re_tok, self._re_inl) - patterns = [re.compile(p % pattern_vars) for p in patterns] - self._re_cache[syntax] = patterns - self.re_split, self.re_tok, self.re_inl = self._re_cache[syntax] - - syntax = property(get_syntax, set_syntax) - - def translate(self): - if self.offset: raise RuntimeError('Parser is a one time instance.') - while True: - m = self.re_split.search(self.source, pos=self.offset) - if m: - text = self.source[self.offset:m.start()] - self.text_buffer.append(text) - self.offset = m.end() - if m.group(1): # Escape syntax - line, sep, _ = self.source[self.offset:].partition('\n') - self.text_buffer.append(self.source[m.start():m.start(1)] + - m.group(2) + line + sep) - self.offset += len(line + sep) - continue - self.flush_text() - self.offset += self.read_code(self.source[self.offset:], - multiline=bool(m.group(4))) - else: - break - self.text_buffer.append(self.source[self.offset:]) - self.flush_text() - return ''.join(self.code_buffer) - - def read_code(self, pysource, multiline): - code_line, comment = '', '' - offset = 0 - while True: - m = self.re_tok.search(pysource, pos=offset) - if not m: - code_line += pysource[offset:] - offset = len(pysource) - self.write_code(code_line.strip(), comment) - break - code_line += pysource[offset:m.start()] - offset = m.end() - _str, _com, _po, _pc, _blk1, _blk2, _end, _cend, _nl = m.groups() - if self.paren_depth > 0 and (_blk1 or _blk2): # a if b else c - code_line += _blk1 or _blk2 - continue - if _str: # Python string - code_line += _str - elif _com: # Python comment (up to EOL) - comment = _com - if multiline and _com.strip().endswith(self._tokens[1]): - multiline = False # Allow end-of-block in comments - elif _po: # open parenthesis - self.paren_depth += 1 - code_line += _po - elif _pc: # close parenthesis - if self.paren_depth > 0: - # we could check for matching parentheses here, but it's - # easier to leave that to python - just check counts - self.paren_depth -= 1 - code_line += _pc - elif _blk1: # Start-block keyword (if/for/while/def/try/...) - code_line = _blk1 - self.indent += 1 - self.indent_mod -= 1 - elif _blk2: # Continue-block keyword (else/elif/except/...) - code_line = _blk2 - self.indent_mod -= 1 - elif _cend: # The end-code-block template token (usually '%>') - if multiline: multiline = False - else: code_line += _cend - elif _end: - self.indent -= 1 - self.indent_mod += 1 - else: # \n - self.write_code(code_line.strip(), comment) - self.lineno += 1 - code_line, comment, self.indent_mod = '', '', 0 - if not multiline: - break - - return offset - - def flush_text(self): - text = ''.join(self.text_buffer) - del self.text_buffer[:] - if not text: return - parts, pos, nl = [], 0, '\\\n' + ' ' * self.indent - for m in self.re_inl.finditer(text): - prefix, pos = text[pos:m.start()], m.end() - if prefix: - parts.append(nl.join(map(repr, prefix.splitlines(True)))) - if prefix.endswith('\n'): parts[-1] += nl - parts.append(self.process_inline(m.group(1).strip())) - if pos < len(text): - prefix = text[pos:] - lines = prefix.splitlines(True) - if lines[-1].endswith('\\\\\n'): lines[-1] = lines[-1][:-3] - elif lines[-1].endswith('\\\\\r\n'): lines[-1] = lines[-1][:-4] - parts.append(nl.join(map(repr, lines))) - code = '_printlist((%s,))' % ', '.join(parts) - self.lineno += code.count('\n') + 1 - self.write_code(code) - - @staticmethod - def process_inline(chunk): - if chunk[0] == '!': return '_str(%s)' % chunk[1:] - return '_escape(%s)' % chunk - - def write_code(self, line, comment=''): - code = ' ' * (self.indent + self.indent_mod) - code += line.lstrip() + comment + '\n' - self.code_buffer.append(code) - - -def template(*args, **kwargs): - """ - Get a rendered template as a string iterator. - You can use a name, a filename or a template string as first parameter. - Template rendering arguments can be passed as dictionaries - or directly (as keyword arguments). - """ - tpl = args[0] if args else None - for dictarg in args[1:]: - kwargs.update(dictarg) - adapter = kwargs.pop('template_adapter', SimpleTemplate) - lookup = kwargs.pop('template_lookup', TEMPLATE_PATH) - tplid = (id(lookup), tpl) - if tplid not in TEMPLATES or DEBUG: - settings = kwargs.pop('template_settings', {}) - if isinstance(tpl, adapter): - TEMPLATES[tplid] = tpl - if settings: TEMPLATES[tplid].prepare(**settings) - elif "\n" in tpl or "{" in tpl or "%" in tpl or '$' in tpl: - TEMPLATES[tplid] = adapter(source=tpl, lookup=lookup, **settings) - else: - TEMPLATES[tplid] = adapter(name=tpl, lookup=lookup, **settings) - if not TEMPLATES[tplid]: - abort(500, 'Template (%s) not found' % tpl) - return TEMPLATES[tplid].render(kwargs) - - -mako_template = functools.partial(template, template_adapter=MakoTemplate) -cheetah_template = functools.partial(template, - template_adapter=CheetahTemplate) -jinja2_template = functools.partial(template, template_adapter=Jinja2Template) - - -def view(tpl_name, **defaults): - """ Decorator: renders a template for a handler. - The handler can control its behavior like that: - - - return a dict of template vars to fill out the template - - return something other than a dict and the view decorator will not - process the template, but return the handler result as is. - This includes returning a HTTPResponse(dict) to get, - for instance, JSON with autojson or other castfilters. - """ - - def decorator(func): - - @functools.wraps(func) - def wrapper(*args, **kwargs): - result = func(*args, **kwargs) - if isinstance(result, (dict, DictMixin)): - tplvars = defaults.copy() - tplvars.update(result) - return template(tpl_name, **tplvars) - elif result is None: - return template(tpl_name, defaults) - return result - - return wrapper - - return decorator - - -mako_view = functools.partial(view, template_adapter=MakoTemplate) -cheetah_view = functools.partial(view, template_adapter=CheetahTemplate) -jinja2_view = functools.partial(view, template_adapter=Jinja2Template) - -############################################################################### -# Constants and Globals ######################################################## -############################################################################### - -TEMPLATE_PATH = ['./', './views/'] -TEMPLATES = {} -DEBUG = False -NORUN = False # If set, run() does nothing. Used by load_app() - -#: A dict to map HTTP status codes (e.g. 404) to phrases (e.g. 'Not Found') -HTTP_CODES = httplib.responses.copy() -HTTP_CODES[418] = "I'm a teapot" # RFC 2324 -HTTP_CODES[428] = "Precondition Required" -HTTP_CODES[429] = "Too Many Requests" -HTTP_CODES[431] = "Request Header Fields Too Large" -HTTP_CODES[451] = "Unavailable For Legal Reasons" # RFC 7725 -HTTP_CODES[511] = "Network Authentication Required" -_HTTP_STATUS_LINES = dict((k, '%d %s' % (k, v)) - for (k, v) in HTTP_CODES.items()) - -#: The default template used for error pages. Override with @error() -ERROR_PAGE_TEMPLATE = """ -%%try: - %%from %s import DEBUG, request - - - - Error: {{e.status}} - - - -

Error: {{e.status}}

-

Sorry, the requested URL {{repr(request.url)}} - caused an error:

-
{{e.body}}
- %%if DEBUG and e.exception: -

Exception:

- %%try: - %%exc = repr(e.exception) - %%except: - %%exc = '' %% type(e.exception).__name__ - %%end -
{{exc}}
- %%end - %%if DEBUG and e.traceback: -

Traceback:

-
{{e.traceback}}
- %%end - - -%%except ImportError: - ImportError: Could not generate the error page. Please add bottle to - the import path. -%%end -""" % __name__ - -#: A thread-safe instance of :class:`LocalRequest`. If accessed from within a -#: request callback, this instance always refers to the *current* request -#: (even on a multi-threaded server). -request = LocalRequest() - -#: A thread-safe instance of :class:`LocalResponse`. It is used to change the -#: HTTP response for the *current* request. -response = LocalResponse() - -#: A thread-safe namespace. Not used by Bottle. -local = threading.local() - -# Initialize app stack (create first empty Bottle app now deferred until needed) -# BC: 0.6.4 and needed for run() -apps = app = default_app = AppStack() - -#: A virtual package that redirects import statements. -#: Example: ``import bottle.ext.sqlite`` actually imports `bottle_sqlite`. -ext = _ImportRedirect('bottle.ext' if __name__ == '__main__' else - __name__ + ".ext", 'bottle_%s').module - - -def _main(argv): # pragma: no coverage - args, parser = _cli_parse(argv) - - def _cli_error(cli_msg): - parser.print_help() - _stderr('\nError: %s\n' % cli_msg) - sys.exit(1) - - if args.version: - _stdout('Bottle %s\n' % __version__) - sys.exit(0) - if not args.app: - _cli_error("No application entry point specified.") - - sys.path.insert(0, '.') - sys.modules.setdefault('bottle', sys.modules['__main__']) - - host, port = (args.bind or 'localhost'), 8080 - if ':' in host and host.rfind(']') < host.rfind(':'): - host, port = host.rsplit(':', 1) - host = host.strip('[]') - - config = ConfigDict() - - for cfile in args.conf or []: - try: - if cfile.endswith('.json'): - with open(cfile, 'rb') as fp: - config.load_dict(json_loads(fp.read())) - else: - config.load_config(cfile) - except configparser.Error as parse_error: - _cli_error(parse_error) - except IOError: - _cli_error("Unable to read config file %r" % cfile) - except (UnicodeError, TypeError, ValueError) as error: - _cli_error("Unable to parse config file %r: %s" % (cfile, error)) - - for cval in args.param or []: - if '=' in cval: - config.update((cval.split('=', 1),)) - else: - config[cval] = True - - run(args.app, - host=host, - port=int(port), - server=args.server, - reloader=args.reload, - plugins=args.plugin, - debug=args.debug, - config=config) - - -if __name__ == '__main__': # pragma: no coverage - _main(sys.argv) diff --git a/deprecated/server/optimizer.py b/deprecated/server/optimizer.py deleted file mode 100644 index a39d552233..0000000000 --- a/deprecated/server/optimizer.py +++ /dev/null @@ -1,106 +0,0 @@ -import types - -import numpy as np - -from pysisyphus.optimizers.poly_fit import poly_line_search - - -def get_step_func(key="sd", alpha=0.1, gdiis=False, line_search=False): - alpha0 = alpha - print("alpha0", alpha0, "gdiis", gdiis, "line_search", line_search) - - def sd_step_func(self, coords, energy, grad): - grad_norm = np.linalg.norm(grad) - direction = -grad / grad_norm - - alpha = min(grad_norm, alpha0) - step = alpha * direction - - return step - - prev_grad = None # lgtm [py/unused-local-variable] - prev_p = None # lgtm [py/unused-local-variable] - prev_energy = None # lgtm [py/unused-local-variable] - prev_step = None # lgtm [py/unused-local-variable] - def cg_step_func(self, coords, energy, grad): - nonlocal prev_grad - nonlocal prev_p - nonlocal prev_energy - nonlocal prev_step - - grad_norm = np.linalg.norm(grad) - alpha = min(grad_norm, alpha0) - - # Steepst descent in the first cycle - if prev_grad is None: - prev_grad = grad - prev_p = -grad - prev_energy = energy - - direction = -grad / grad_norm - step = alpha * direction - prev_step = step - return step - - if line_search: - fit_step, fit_grad, fit_energy = poly_line_search(energy, prev_energy, - grad, prev_grad, - prev_step, coords) - # Polak-Ribierre - beta = grad.dot(grad-prev_grad) / prev_grad.dot(prev_grad) - p = -grad + beta*prev_p - - if line_search and fit_step is not None: - step = fit_step - grad = fit_grad - energy = fit_energy - else: - step = np.linalg.norm(prev_step) * p / np.linalg.norm(p) - - prev_grad = grad - prev_energy = energy - prev_p = p - prev_step = step - - return step - - step_funcs = { - "sd": sd_step_func, - "cg": cg_step_func, - } - return step_funcs[key] - - -class OptState: - - def __init__(self, key="sd", alpha=0.3, gdiis=False, line_search=True): - self.key = key - self.alpha = alpha - self.gdiis = gdiis - self.line_search = line_search - - self.coords = list() - self.grads = list() - self.energies = list() - self.steps = list() - - step_func_kwargs = { - "key": self.key, - "alpha": self.alpha, - "gdiis": self.gdiis, - "line_search": self.line_search, - } - step_func = get_step_func(**step_func_kwargs) - # Register step_func - self.step_func = types.MethodType(step_func, self) - - print("Initialized OptState") - - def step(self, coords, energy, grad): - self.coords.append(coords) - self.energies.append(energy) - self.grads.append(grad) - - step = self.step_func(coords, energy, grad) - - return step, "SUCCESS" diff --git a/deprecated/tests/test_baker_ts/test_baker_ts.py b/deprecated/tests/test_baker_ts/test_baker_ts.py deleted file mode 100644 index 8eabcf2834..0000000000 --- a/deprecated/tests/test_baker_ts/test_baker_ts.py +++ /dev/null @@ -1,207 +0,0 @@ -#!/usr/bin/env python3 - -import itertools as it -from pathlib import Path -from pprint import pprint -import shutil -import time - -import numpy as np -import pandas as pd - -from pysisyphus.calculators.Gaussian16 import Gaussian16 -from pysisyphus.calculators.PySCF import PySCF -from pysisyphus.color import red, green -from pysisyphus.helpers import get_baker_ts_geoms, do_final_hessian, \ - geom_from_library, get_baker_ts_geoms_flat, \ - geom_loader -from pysisyphus.intcoords.augment_bonds import augment_bonds -from pysisyphus.tsoptimizers import * - - -def print_summary(converged, failed, cycles, ran, runid): - ran_ = f"{ran+1:02d}" - print(f"converged: {converged:02d}/{ran_}") - print(f" failed: {failed:d}") - print(f" cycles: {cycles}") - print(f" run: {runid}") - - -def run_baker_ts_opts(geoms, meta, coord_type="cart", thresh="baker", runid=0): - """From 10.1002/(SICI)1096-987X(199605)17:7<888::AID-JCC12>3.0.CO;2-7""" - start = time.time() - - converged = 0 - failed = 0 - cycles = 0 - opt_kwargs = { - "thresh": thresh, - # "max_cycles": 150, - "max_cycles": 100, - # "max_cycles": 50, - "dump": True, - "trust_radius": 0.3, - "trust_max": 0.3, - # "max_micro_cycles": 1, - } - results = dict() - for i, (name, geom) in enumerate(geoms.items()): - print(f"@Running {name}") - charge, mult, ref_energy = meta[name] - calc_kwargs = { - "charge": charge, - "mult": mult, - "pal": 4, - } - geom.set_calculator(Gaussian16(route="HF/3-21G", **calc_kwargs)) - geom = augment_bonds(geom) - # geom.set_calculator(PySCF(basis="321g", **calc_kwargs)) - - # opt = RSPRFOptimizer(geom, **opt_kwargs) - opt = RSIRFOptimizer(geom, **opt_kwargs) - # opt = RSIRFOptimizer(geom, **opt_kwargs) - # opt = TRIM(geom, **opt_kwargs) - opt.run() - if opt.is_converged: - converged += 1 - else: - failed += 1 - cycles += opt.cur_cycle + 1 - energies_match = np.allclose(geom.energy, ref_energy) - try: - assert np.allclose(geom.energy, ref_energy) - # Backup TS if optimization succeeded - # ts_xyz_fn = Path(name).stem + "_opt_ts.xyz" - # out_path = Path("/scratch/programme/pysisyphus/xyz_files/baker_ts_opt/") - print(green(f"\t@Energies MATCH for {name}! ({geom.energy:.6f}, {ref_energy:.6f})")) - # with open(out_path / ts_xyz_fn, "w") as handle: - # handle.write(geom.as_xyz()) - except AssertionError as err: - print(red(f"\t@Calculated energy {geom.energy:.6f} and reference " - f"energy {ref_energy:.6f} DON'T MATCH'.")) - print() - print_summary(converged & energies_match, failed, cycles, i, runid) - print() - results[name] = (opt.cur_cycle + 1, opt.is_converged) - pprint(results) - print() - # do_final_hessian(geom, False) - # print() - - end = time.time() - duration = end - start - - print(f" runtime: {duration:.1f} s") - print_summary(converged, failed, cycles, i, runid) - return results, duration, cycles - - -@pytest.mark.benchmark -@using_gaussian16 -def _test_baker_ts_optimizations(): - coord_type = "redund" - # coord_type = "dlc" - # coord_type = "cart" - thresh = "baker" - runs = 1 - - all_results = list() - durations = list() - all_cycles = list() - for i in range(runs): - geoms, meta = get_baker_ts_geoms(coord_type=coord_type) - # only = "01_hcn.xyz" - # only = "24_h2cnh.xyz" - # only = "15_hocl.xyz" - # only = "02_hcch.xyz" - # geoms = { - # only: geoms[only], - # } - - fails = ( - "09_parentdieslalder.xyz", - "12_ethane_h2_abstraction.xyz", - "22_hconhoh.xyz", - "17_claisen.xyz", - "15_hocl.xyz", - ) - works = ( - "05_cyclopropyl.xyz", - "08_formyloxyethyl.xyz", - "14_vinyl_alcohol.xyz", - "16_h2po4_anion.xyz", - "18_silyene_insertion.xyz", - "04_ch3o.xyz", - "06_bicyclobutane.xyz", - "07_bicyclobutane.xyz", - "23_hcn_h2.xyz", - "01_hcn.xyz", - "25_hcnh2.xyz", - ) - math_error_but_works = ( - # [..]/intcoords/derivatives.py", line 640, in d2q_d - # x99 = 1/sqrt(x93) - # ValueError: math domain error - # ZeroDivison Fix - "20_hconh3_cation.xyz", - "24_h2cnh.xyz", - "13_hf_abstraction.xyz", - "19_hnccs.xyz", - "21_acrolein_rot.xyz", - "03_h2co.xyz", - ) - alpha_negative = ( - "02_hcch.xyz", - ) - no_imag = ( - "10_tetrazine.xyz", - "11_trans_butadiene.xyz", - ) - only = ( - "18_silyene_insertion.xyz", - # "21_acrolein_rot.xyz", - # "22_hconhoh.xyz", - ) - use = ( - # fails, - works, - math_error_but_works, - # alpha_negative, - # no_imag, - # only, - ) - geoms = {key: geoms[key] for key in it.chain(*use)} - - # geoms = {"05_cyclopropyl.xyz": geoms["05_cyclopropyl.xyz"]} - - results, duration, cycles = run_baker_ts_opts( - geoms, - meta, - coord_type, - thresh, - runid=i - ) - all_results.append(results) - durations.append(duration) - all_cycles.append(cycles) - print(f"@Run {i}, {cycles} cycles") - print(f"@All cycles: {all_cycles}") - print(f"@This runtime: {duration:.1f} s") - print(f"@Total runtime: {sum(durations):.1f} s") - print(f"@") - return - - names = list(results.keys()) - cycles = { - name: [result[name][0] for result in all_results] for name in names - } - df = pd.DataFrame.from_dict(cycles) - - df_name = f"cycles_{coord_type}_{runs}_runs_{thresh}.pickle" - df.to_pickle(df_name) - print(f"Pickled dataframe to {df_name}") - print(f"{runs} runs took {sum(durations):.1f} seconds.") - - -if __name__ == "__main__": - _test_baker_ts_optimizations() diff --git a/deprecated/tests/test_baker_ts_dimer/test_baker_ts_dimer.py b/deprecated/tests/test_baker_ts_dimer/test_baker_ts_dimer.py deleted file mode 100644 index 1fea6bdb55..0000000000 --- a/deprecated/tests/test_baker_ts_dimer/test_baker_ts_dimer.py +++ /dev/null @@ -1,86 +0,0 @@ -from pathlib import Path -import os - -from natsort import natsorted -import numpy as np -import pytest - -from pysisyphus.benchmarks import Benchmark -from pysisyphus.calculators import Dimer, Gaussian16 -from pysisyphus.calculators.PySCF import PySCF -from pysisyphus.Geometry import Geometry -from pysisyphus.helpers import geom_from_xyz_file, geom_from_library, do_final_hessian -from pysisyphus.tsoptimizers.dimer import dimer_method -from pysisyphus.tsoptimizers.dimerv2 import dimer_method as dimer_method_v2 -from pysisyphus.optimizers.PreconLBFGS import PreconLBFGS -from pysisyphus.testing import using - - -def make_N_init_dict(): - THIS_DIR = Path(os.path.abspath(os.path.dirname(__file__))) - xyz_path = THIS_DIR / "../../xyz_files/baker_ts" - xyzs = natsorted(xyz_path.glob("*.xyz")) - N_dict = dict() - for guess, initial in [xyzs[2 * i : 2 * i + 2] for i in range(25)]: - assert "downhill" in initial.stem - assert guess.stem[:2] == initial.stem[:2] - guess_geom = geom_from_xyz_file(guess) - initial_geom = geom_from_xyz_file(initial) - N_init = guess_geom.coords - initial_geom.coords - N_dict[guess.name] = N_init - return N_dict - - - -BakerTSBm = Benchmark( - "baker_ts", coord_type="cart", exclude=(4, 9, 10, 14, 16, 17, 19) -) - - -@using("pyscf") -@pytest.mark.parametrize("fn, geom, charge, mult, ref_energy", BakerTSBm.geom_iter) -def test_baker_ts_dimer(fn, geom, charge, mult, ref_energy, results_bag): - # Load initial dimers directions - N_init_dict = make_N_init_dict() - - calc_kwargs = { - "charge": charge, - "mult": mult, - "pal": 2, - "base_name": Path(fn).stem, - } - - def calc_getter(): - return PySCF(basis="321g", verbose=0, **calc_kwargs) - - geom.set_calculator(calc_getter()) - - dimer_kwargs = { - "max_step": 0.25, - # 1e-2 Angstroem in bohr - "dR_base": 0.0189, - "rot_opt": "lbfgs", - # "trans_opt": "mb", - "trans_opt": "lbfgs", - # "trans_memory": 10, # bad idea - "angle_tol": 5, - "f_thresh": 1e-3, - "max_cycles": 50, - "f_tran_mod": True, - # "rot_type": "direct", - "multiple_translations": True, - } - dimer_kwargs["N_init"] = N_init_dict[fn] - geoms = (geom,) - results = dimer_method(geoms, calc_getter, **dimer_kwargs) - - same_energy = geom.energy == pytest.approx(ref_energy) - print( - f"@Same energy: {str(same_energy): >5}, {fn}, " - f"{results.force_evals} force evaluations" - ) - if not same_energy: - do_final_hessian(geom) - - # This way pytest prints the actual values... instead of just the boolean - assert geom.energy == pytest.approx(ref_energy) diff --git a/deprecated/tests/test_bottle/runopt.py b/deprecated/tests/test_bottle/runopt.py deleted file mode 100644 index 6e6429e085..0000000000 --- a/deprecated/tests/test_bottle/runopt.py +++ /dev/null @@ -1,82 +0,0 @@ -import matplotlib.pyplot as plt -import numpy as np -import requests -import simplejson as json - -from pysisyphus.calculators.AnaPot import AnaPot -from pysisyphus.server.optimizer import OptState - - -def run(): - geom = AnaPot.get_geom((-1, 3, 0)) - print(geom) - - def prep_post(geom): - return { - "coords": geom.coords.tolist(), - "gradient": geom.gradient.tolist(), - "energy": float(geom.energy), - } - - req_kwargs = { - "headers": {"Content-type": "application/json", }, - } - base_url = "http://localhost:8080/" - def send_json(path, data): - url = base_url + path - response = requests.post(url=url, data=json.dumps(data), **req_kwargs) - return response - - - init = { - "key": "cg", - "alpha": 0.1, - "gdiis": False, - "line_search": False, - } - init_resp = send_json("init", init) - print("Init response", init_resp) - - coords = list() - opt_state = OptState(alpha=0.3, key="cg") - - for i in range(50): - coords.append(geom.coords) - print("\tcur. coords", geom.coords) - gradient = geom.gradient - norm = np.linalg.norm(gradient) - - print(f"{i:02d}: norm(grad)={norm:.6f}") - if norm <= 0.1: - print("Converged") - break - - pp = prep_post(geom) - # # resp = requests.post(**req_kwargs, data=pp) - # resp = send_json("step", pp) - # resp_data = resp.json() - - # step = resp_data["step"] - # status = resp_data["status"] - step, status = opt_state.step(geom.coords, geom.energy, geom.gradient) - new_coords = geom.coords + step - # print("\t", status) - geom.coords = new_coords - print() - print() - - # import pdb; pdb.set_trace() - # pass - - coords = np.array(coords) - calc = geom.calculator - calc.plot() - ax = calc.ax - ax.plot(*coords.T[:2], "ro-") - for i, xy in enumerate(coords[:,:2]): - ax.annotate(str(i), xy) - plt.show() - - -if __name__ == "__main__": - run() diff --git a/deprecated/tests/test_dynamics/test_dynamics.py b/deprecated/tests/test_dynamics/test_dynamics.py deleted file mode 100644 index b66caee0a0..0000000000 --- a/deprecated/tests/test_dynamics/test_dynamics.py +++ /dev/null @@ -1,92 +0,0 @@ -from matplotlib.patches import Circle -import matplotlib.pyplot as plt -import numpy as np -import pytest - -from pysisyphus.calculators.AnaPot import AnaPot -from pysisyphus.dynamics.velocity_verlet import md - - -def test_velocity_verlet(): - geom = AnaPot.get_geom((0.52, 1.80, 0)) - x0 = geom.coords.copy() - v0 = .1 * np.random.rand(*geom.coords.shape) - t = 3 - dts = (.005, .01, .02, .04, .08) - all_xs = list() - for dt in dts: - geom.coords = x0.copy() - md_kwargs = { - "v0": v0.copy(), - "t": t, - "dt": dt, - } - md_result = md(geom, **md_kwargs) - all_xs.append(md_result.coords) - calc = geom.calculator - calc.plot() - ax = calc.ax - for dt, xs in zip(dts, all_xs): - ax.plot(*xs.T[:2], "o-", label=f"dt={dt:.3f}") - # ax.plot(*xs.T[:2], "-", label=f"dt={dt:.3f}") - ax.legend() - plt.show() - - -def ase_md_playground(): - geom = AnaPot.get_geom((0.52, 1.80, 0), atoms=("H", )) - atoms = geom.as_ase_atoms() - # ase_calc = FakeASE(geom.calculator) - # from ase.optimize import BFGS - # dyn = BFGS(atoms) - # dyn.run(fmax=0.05) - - import ase - from ase import units - from ase.io.trajectory import Trajectory - from ase.md.velocitydistribution import MaxwellBoltzmannDistribution - from ase.md.verlet import VelocityVerlet - - MaxwellBoltzmannDistribution(atoms, 300 * units.kB) - momenta = atoms.get_momenta() - momenta[0, 2] = 0. - # Zero 3rd dimension - atoms.set_momenta(momenta) - - dyn = VelocityVerlet(atoms, .005 * units.fs) # 5 fs time step. - - - def printenergy(a): - """Function to print the potential, kinetic and total energy""" - epot = a.get_potential_energy() / len(a) - ekin = a.get_kinetic_energy() / len(a) - print('Energy per atom: Epot = %.3feV Ekin = %.3feV (T=%3.0fK) ' - 'Etot = %.3feV' % (epot, ekin, ekin / (1.5 * units.kB), epot + ekin)) - - # Now run the dynamics - printenergy(atoms) - traj_fn = 'asemd.traj' - traj = Trajectory(traj_fn, 'w', atoms) - dyn.attach(traj.write, interval=5) - # dyn.attach(bumms().bimms, interval=1) - - dyn.run(10000) - printenergy(atoms) - traj.close() - - traj = ase.io.read(traj_fn+"@:")#, "r") - pos = [a.get_positions() for a in traj] - from pysisyphus.constants import BOHR2ANG - pos = np.array(pos) / BOHR2ANG - - calc = geom.calculator - calc.plot() - - ax = calc.ax - ax.plot(*pos[:,0,:2].T) - - plt.show() - - -if __name__ == "__main__": - ase_md_playground() diff --git a/deprecated/tsoptimizers/GAD.py b/deprecated/tsoptimizers/GAD.py deleted file mode 100644 index 4ebd5057c6..0000000000 --- a/deprecated/tsoptimizers/GAD.py +++ /dev/null @@ -1,39 +0,0 @@ -import numpy as np - -from pysisyphus.optimizers.HessianOptimizer import HessianOptimizer - - -class GAD(HessianOptimizer): - - def __init__(self, geom, dt=0.1, **kwargs): - super().__init__(geom, **kwargs) - - self.dt = dt - - def prepare_opt(self): - super().prepare_opt() - - # Usually eigenvector of H matrix - v = self.geometry.gradient - self.v = v / np.linalg.norm(v) - - self.I = np.eye(self.geometry.coords.size) - - def optimize(self): - grad = self.geometry.gradient - self.forces.append(-grad) - hessian = self.geometry.hessian - - P = np.outer(self.v, self.v) - - dxdt = -(self.I - 2*P) @ grad - dvdt = -(self.I - P) @ hessian @ self.v - dv = dvdt * self.dt - v = self.v + dv - self.v = v / np.linalg.norm(v) - - step = dxdt * self.dt - step_norm = np.linalg.norm(step) - if step_norm > self.dt: - step = self.dt * step / step_norm - return step diff --git a/deprecated/tsoptimizers/dimer.py b/deprecated/tsoptimizers/dimer.py deleted file mode 100644 index 9b6a956a65..0000000000 --- a/deprecated/tsoptimizers/dimer.py +++ /dev/null @@ -1,544 +0,0 @@ -from collections import namedtuple -import logging -import sys - -import cloudpickle -import numpy as np - -from pysisyphus.constants import EVANG2AUBOHR -from pysisyphus.helpers import check_for_end_sign, get_geom_getter -from pysisyphus.helpers_pure import highlight_text -from pysisyphus.optimizers.closures import lbfgs_closure_ -import pysisyphus.optimizers.closures as closures -from pysisyphus.TablePrinter import TablePrinter - - -logger = logging.getLogger("tsoptimizer") - - -DimerCycle = namedtuple("DimerCycle", - "org_coords rot_coords trans_coords f0 f_tran", -) - -DimerPickle = namedtuple("DimerPickle", - "coords0 N dR" -) - -DimerResult = namedtuple("DimerResult", - "dimer_cycles force_evals geom0 converged") - -def make_unit_vec(vec1, vec2): - """Return unit vector pointing from vec2 to vec1.""" - diff = vec1 - vec2 - return diff / np.linalg.norm(diff) - - -def perpendicular_force(force, vec): - """Return the perpendicular component of force along vec.""" - return force - force.dot(vec)*vec - - -def rotate_R1(coords0, rad, N, theta, dR): - """Rotate dimer at coords0 to get a new coords1. - - Only valid for rotation of R1!""" - return coords0 + (N*np.cos(rad) + theta*np.sin(rad)) * dR - - -def get_curvature(f1, f2, N, dR): - return (f2 - f1).dot(N) / (2*dR) - - -def get_rms(arr): - return np.sqrt(np.mean(np.square(arr))) - - -def get_lambda(f_parallel): - rms = get_rms(f_parallel) - - if rms < 0.5*EVANG2AUBOHR: - l_ = 1. - elif rms < 1.0*EVANG2AUBOHR: - l_ = 0.5 - elif rms < 2.0*EVANG2AUBOHR: - l_ = 0.25 - else: - l_ = 0.1 - return l_ - - -def get_f_tran_mod(f, N, C): - """Eq. (15) and (16) from [5].""" - f_parallel = f.dot(N)*N - f_perp = f - f_parallel - if C < 0: - lambda_ = get_lambda(f_parallel) - f_tran = f_perp - lambda_ * f_parallel - else: - perp_rms = get_rms(f_perp) - if perp_rms < 2*EVANG2AUBOHR: - f_tran = 0.5*f_perp - f_parallel - else: - f_tran = f_perp - 0.5*f_parallel - # f_tran = -f_parallel - return f_tran, f_parallel, f_perp - - -def get_f_tran_org(f, N, C): - f_parallel = f.dot(N)*N - f_perp = f - f_parallel - if C < 0: - f_tran = f_perp - f_parallel - else: - f_tran = -f_parallel - return f_tran, f_parallel, f_perp - - # return f_tran - # WTH; It seems to make a difference if I return f_tran - # or f_mod here ... wtf? This only happens when MB is used - # for the translation... - # f_mod = -f.dot(N)*N - # if C < 0: - # f_mod = f + 2*f_mod - # np.testing.assert_allclose(f_mod, f_tran) - # print(f"diff={np.linalg.norm(f_tran-f_mod):.4e}") - # print(f"type(f_mod)={type(f_mod)}") - # return f_mod - - -def get_f_perp(f1, f2, N): - f1_perp = perpendicular_force(f1, N) - f2_perp = perpendicular_force(f2, N) - f_perp = f1_perp - f2_perp - return f_perp - - -def get_theta0(f1, f2, N): - """Construct vector theta from f1 and f2.""" - f_perp = get_f_perp(f1, f2, N) - theta = f_perp / np.linalg.norm(f_perp) - return theta - - -def write_progress(geom0): - ts_fn = "dimer_ts.xyz" - ts_xyz_str = geom0.as_xyz() - with open(ts_fn, "w") as handle: - handle.write(ts_xyz_str) - logger.debug(f"Wrote current TS geometry to '{ts_fn}'.") - - -def dimer_method(geoms, calc_getter, N_init=None, - max_step=0.1, max_cycles=50, - max_rots=10, interpolate=True, - rot_type="fourier", - rot_opt="lbfgs", trans_opt="lbfgs", - trans_memory=5, - trial_angle=5, angle_tol=0.5, dR_base=0.01, - restrict_step="scale", ana_2dpot=False, - f_thresh=1e-3, rot_f_thresh=2e-3, - zero_weights=[], dimer_pickle=None, - f_tran_mod=True, - multiple_translations=False, max_translations=10): - """Dimer method using steepest descent for rotation and translation. - - See - # Original paper - [1] https://doi.org/10.1063/1.480097 - # Improved dimer - [2] https://doi.org/10.1063/1.2104507 - # Several trial rotations - [3] https://doi.org/10.1063/1.1809574 - # Superlinear dimer - [4] https://doi.org/10.1063/1.2815812 - # Modified Broyden - [5] https://doi.org/10.1021/ct9005147 - - To add: - Comparing curvatures and adding π/2 if appropriate. - - Default parameters from [1] - max_step = 0.1 bohr - dR_base = = 0.01 bohr - """ - # Parameters - rad_tol = np.deg2rad(angle_tol) - rms_f_thresh = float(f_thresh) - max_f_thresh = 1.5 * rms_f_thresh - max_translations = max_translations if multiple_translations else 1 - - opt_name_dict = { - "cg": "conjugate gradient", - "lbfgs": "L-BFGS", - "mb": "modified Broyden", - "sd": "steepst descent", - } - assert rot_opt in opt_name_dict.keys() - assert rot_type in "fourier direct".split() - # Translation using L-BFGS as described in [4] - # Modified broyden as proposed in [5] 10.1021/ct9005147 - trans_closures = { - "lbfgs": closures.lbfgs_closure_, - "mb": closures.modified_broyden_closure, - } - print(f"Using {opt_name_dict[rot_opt]} for dimer rotation.") - print(f"Using {opt_name_dict[trans_opt]} for dimer translation.") - print(f"Keeping information of last {trans_memory} cycles for " - "translation optimization.") - - f_tran_dict = { - True: get_f_tran_mod, - False: get_f_tran_org - } - get_f_tran = f_tran_dict[f_tran_mod] - - rot_header = "Rot._cycle Curvature rms(f_rot)".split() - rot_fmts = "int float float".split() - rot_table = TablePrinter(rot_header, rot_fmts, width=16, shift_left=2) - trans_header = "Trans._cycle Curvature max(|f0|) rms(f0) rms(step)".split() - trans_fmts = "int float float float float".split() - trans_table = TablePrinter(trans_header, trans_fmts, width=16) - - geom_getter = get_geom_getter(geoms[0], calc_getter) - - assert len(geoms) in (1, 2), "geoms argument must be of length 1 or 2!" - # if dimer_pickle: - # with open(dimer_pickle, "rb") as handle: - # dimer_tuple = cloudpickle.load(handle) - if len(geoms) == 2: - geom1, geom2 = geoms - dR = np.linalg.norm(geom1.coords - geom2.coords) / 2 - # N points from geom2 to geom1 - N = make_unit_vec(geom1.coords, geom2.coords) - coords0 = geom2.coords + dR*N - geom0 = geom_getter(coords0) - # This handles cases where only one geometry is supplied. We use the - # given geometry as midpoint, and use the given N_init. If N_init is - # none, we select a random direction and derive geom1 and geom2 from it. - else: - geom0 = geoms[0] - coords0 = geom0.coords - # Assign random unit vector and use dR_base to for dR - coord_size = geom0.coords.size - if N_init is None: - N_init = np.random.rand(coord_size) - N = np.array(N_init) - if ana_2dpot: - N[2] = 0 - N /= np.linalg.norm(N) - coords1 = geom0.coords + dR_base*N - geom1 = geom_getter(coords1) - coords2 = geom0.coords - dR_base*N - geom2 = geom_getter(coords2) - dR = dR_base - - geom0.calculator.base_name += "_image0" - geom1.calculator.base_name += "_image1" - geom2.calculator.base_name += "_image2" - - dimer_pickle = DimerPickle(coords0, N, dR) - with open("dimer_pickle", "wb") as handle: - cloudpickle.dump(dimer_pickle, handle) - - dimer_cycles = list() - - print("Using N:", N) - def f_tran_getter(coords, N, C): - # The force for the given coord should be already set. - np.testing.assert_allclose(geom0.coords, coords) - # Only return f_tran and drop the parallel and perpendicular component. - return get_f_tran(geom0.forces, N, C)[0] - - def rot_force_getter(N, f1, f2): - return get_f_perp(f1, f2, N) - - def restrict_max_step_comp(x, step, max_step=max_step): - step_max = np.abs(step).max() - if step_max > max_step: - factor = max_step / step_max - step *= factor - return step - - def restrict_step_length(x, step, max_step_length=max_step): - step_norm = np.linalg.norm(step) - if step_norm > max_step_length: - step_direction = step / step_norm - step = step_direction * max_step_length - return step - - rstr_dict = { - "max": restrict_max_step_comp, - "scale": restrict_step_length, - } - rstr_func = rstr_dict[restrict_step] - - tot_rot_force_evals = 0 - # Create the translation optimizer in the first cycle of the loop. - trans_opt_kwargs = { - "restrict_step": rstr_func, - "M": trans_memory, - # "beta": 0.01, - } - if trans_opt == "mb": - trans_opt_kwargs["beta"] = 0.01 - trans_optimizer = trans_closures[trans_opt](f_tran_getter, **trans_opt_kwargs) - - def cbm_rot_force_getter(coords1, N, f1, f2): - return get_f_perp(f1, f2, N) - converged = False - # In the first dimer cycle f0 and f1 aren't set and have to be calculated. - # For cycles i > 0 f0 and f1 will be already set here. - add_force_evals = 2 - for i in range(max_cycles): - logger.debug(f"Dimer macro cycle {i:03d}") - print(highlight_text(f"Dimer Cycle {i:03d}")) - f0 = geom0.forces - f1 = geom1.forces - f2 = 2*f0 - f1 - - coords0 = geom0.coords - coords1 = geom1.coords - coords2 = geom2.coords - - C = get_curvature(f1, f2, N, dR) - - f0_rms = get_rms(f0) - f0_max = np.abs(f0).max() - trans_table.print(f"@ {i:03d}: C={C:.6f}; max(|f0|)={f0_max:.6f}; rms(f0)={f0_rms:.6f}") - print() - converged = C < 0 and f0_rms <= rms_f_thresh and f0_max <= max_f_thresh - if converged: - rot_table.print("@ Converged!") - break - - rot_force_evals = 0 - rot_force0 = get_f_perp(f1, f2, N) - # Initialize some data structure for the rotation optimizers - if rot_opt == "cg": - # Lists for conjugate gradient - G_perps = [rot_force0, ] - F_rots = [rot_force0, ] - # LBFGS optimizer - elif rot_opt == "lbfgs" : - rot_lbfgs = lbfgs_closure_(rot_force_getter) - # Modified broyden - elif rot_opt == "mb": - rot_mb = closures.modified_broyden_closure(rot_force_getter) - - def cbm_restrict(coords1, step, coords0=coords0, dR=dR): - """Constrain of R1 back on hypersphere.""" - coords1_rot = coords1 + step - coords1_dir = coords1_rot - coords0 - coords1_dir /= np.linalg.norm(coords1_dir) - coords1_rot = coords0 + coords1_dir*dR - return coords1_rot - coords1 - rot_cbm_kwargs = { - "restrict_step": cbm_restrict, - "beta": 0.01, - "M": 3, - } - rot_cbm = closures.modified_broyden_closure(cbm_rot_force_getter, **rot_cbm_kwargs) - for j in range(max_rots): - logger.debug(f"Rotation cycle {j:02d}") - if rot_type == "fourier": - C = get_curvature(f1, f2, N, dR) - logger.debug(f"C={C:.6f}") - # Theta from L-BFGS as implemented in DL-FIND dlf_dimer.f90 - if rot_opt == "lbfgs": - theta_dir, rot_force = rot_lbfgs(N, f1, f2) - elif rot_opt == "mb": - theta_dir, rot_force = rot_mb(N, f1, f2) - # Theta from conjugate gradient - elif j > 0 and rot_opt == "cg": - # f_perp = weights.dot(get_f_perp(f1, f2, N)) - f_perp = get_f_perp(f1, f2, N) - rot_force = f_perp - gamma = (f_perp - F_rots[-1]).dot(f_perp) / f_perp.dot(f_perp) - G_last = G_perps[-1] - # theta will be present with j > 0. - G_perp = f_perp + gamma * np.linalg.norm(G_last)*theta # noqa: F821 - theta_dir = G_perp - F_rots.append(f_perp) - G_perps.append(G_perp) - # Theta from plain steepest descent F_rot/|F_rot| - else: - # theta_dir = weights.dot(get_f_perp(f1, f2, N)) - theta_dir = get_f_perp(f1, f2, N) - # Remove component that is parallel to N - theta_dir = theta_dir - theta_dir.dot(N)*N - theta = theta_dir / np.linalg.norm(theta_dir) - - # Get rotated endpoint geometries. The rotation takes place in a plane - # spanned by N and theta. Theta is a unit vector perpendicular to N that - # can be formed from the perpendicular components of the forces at the - # endpoints. - - # Derivative of the curvature, Eq. (29) in [2] - # (f2 - f1) or -(f1 - f2) - dC = 2*(f0-f1).dot(theta)/dR - rad_trial = -0.5*np.arctan2(dC, 2*abs(C)) - logger.debug(f"rad_trial={rad_trial:.2f}") - # print(f"rad_trial={rad_trial:.2f} rad_tol={rad_tol:.2f}") - if np.abs(rad_trial) < rad_tol: - logger.debug(f"rad_trial={rad_trial:.2f} below threshold. Breaking.") - break - - # Trial rotation for finite difference calculation of rotational force - # and rotational curvature. - coords1_trial = rotate_R1(coords0, rad_trial, N, theta, dR) - f1_trial = geom1.get_energy_and_forces_at(coords1_trial)["forces"] - rot_force_evals += 1 - f2_trial = 2*f0 - f1_trial - N_trial = make_unit_vec(coords1_trial, coords0) - - C_trial = get_curvature(f1_trial, f2_trial, N_trial, dR) - - b1 = 0.5 * dC - a1 = (C - C_trial + b1*np.sin(2*rad_trial)) / (1-np.cos(2*rad_trial)) - a0 = 2 * (C - a1) - - rad_min = 0.5 * np.arctan(b1/a1) - logger.debug(f"rad_min={rad_min:.2f}") - def get_C(theta_rad): - return a0/2 + a1*np.cos(2*theta_rad) + b1*np.sin(2*theta_rad) - C_min = get_C(rad_min) # lgtm [py/multiple-definition] - if C_min > C: - rad_min += np.deg2rad(90) - C_min_new = get_C(rad_min) - logger.debug( "Predicted theta_min lead us to a curvature maximum " - f"(C(theta)={C_min:.6f}). Adding pi/2 to theta_min. " - f"(C(theta+pi/2)={C_min_new:.6f})" - ) - C_min = C_min_new - - # TODO: handle cases where the curvature is still positive, but - # the angle is small, so the rotation is skipped. - # Don't do rotation for small angles - if np.abs(rad_min) < rad_tol: - logger.debug(f"rad_min={rad_min:.2f} below threshold. Breaking.") - break - coords1_rot = rotate_R1(coords0, rad_min, N, theta, dR) - - # Interpolate force at coords1_rot; see Eq. (12) in [4] - if interpolate: - f1 = (np.sin(rad_trial-rad_min)/np.sin(rad_trial)*f1 - + np.sin(rad_min)/np.sin(rad_trial)*f1_trial - + (1 - np.cos(rad_min) - np.sin(rad_min) - * np.tan(rad_trial/2))*f0 - ) - else: - f1 = geom1.get_energy_and_forces_at(coords1_rot)["forces"] - rot_force_evals += 1 - elif rot_type == "direct": - rot_force = get_f_perp(f1, f2, N) - rot_force_rms = get_rms(rot_force) - # print(f"rot_force_rms={rot_force_rms:.4e}, thresh={rot_f_thresh:.4e}") - if rot_force_rms <= rot_f_thresh: - break - rot_step, rot_f = rot_cbm(coords1, N, f1, f2) - coords1_rot = coords1 + rot_step - geom1.coords = coords1_rot - coords1 = coords1_rot - f1 = geom1.forces - rot_force_evals += 1 - else: - raise NotImplementedError("Invalid 'rot_type'!") - - N = make_unit_vec(coords1_rot, coords0) - coords2_rot = coords0 - N*dR - f2 = 2*f0 - f1 - C = get_curvature(f1, f2, N, dR) - rot_force = get_f_perp(f1, f2, N) - rot_force_rms = get_rms(rot_force) - logger.debug("") - if j == 0: - rot_table.print_header() - rot_table.print_row((j, C, rot_force_rms)) - tot_rot_force_evals += rot_force_evals - rot_str = (f"Did {rot_force_evals} force evaluation(s) and {j} " - "dimer rotation(s).") - rot_table.print(rot_str) - logger.debug(rot_str) - print() - - # If multiple_translations == False then max_translations is 1 - # and we will do only one iteration. - for trans_i in range(max_translations): - _, f_parallel, f_perp = get_f_tran(f0, N, C) - prev_f_par_rms = get_rms(f_parallel) - # prev_f_perp_rms = get_rms(f_perp) - prev_C = C - f0_rms = get_rms(f0) - f0_max = max(np.abs(f0)) - converged = C < 0 and f0_rms <= rms_f_thresh and f0_max <= max_f_thresh - if converged: - break - step, f_tran = trans_optimizer(coords0, N, C) - step_rms = get_rms(step) - coords0_trans = coords0 + step - coords1_trans = coords0_trans + dR*N - coords2_trans = coords0_trans - dR*N - geom0.coords = coords0_trans - geom1.coords = coords1_trans - geom2.coords = coords2_trans - coords0 = geom0.coords - # Calculate new forces for translated dimer - f0 = geom0.forces - f1 = geom1.forces - add_force_evals += 2 - f2 = 2*f0 - f1 - C = get_curvature(f1, f2, N, dR) - f0_rms = get_rms(f0) - f0_max = max(np.abs(f0)) - if multiple_translations: - if trans_i == 0: - trans_table.print_header() - trans_args = (trans_i, C, f0_max, f0_rms, step_rms) - trans_table.print_row(trans_args) - else: - trans_table.print("Did dimer translation.") - _, f_parallel, f_perp = get_f_tran(f0, N, C) - f_par_rms = get_rms(f_parallel) - # f_perp_rms = get_rms(f_perp) - # Check for sign change of curvature - if (prev_C < 0) and (np.sign(C/prev_C) < 0): - trans_table.print("Curvature became positive!") - break - if (C < 0) and (f_par_rms > prev_f_par_rms): - break - # elif ((C > 0) and (f_par_rms < prev_f_par_rms) - # or (f_perp_rms > prev_f_perp_rms)): - elif (C > 0) and (f_par_rms < prev_f_par_rms): - break - prev_f_par_rms = f_par_rms # lgtm [py/multiple-definition] - # prev_f_perp_rms = f_perp_rms - - # Save cycle information - org_coords = np.array((coords1, coords0, coords2)) - try: - rot_coords = np.array((coords1_rot, coords0, coords2_rot)) - except NameError: - rot_coords = np.array((coords1, coords0, coords2)) - trans_coords = np.array((coords1_trans, coords0_trans, coords2_trans)) - dc = DimerCycle(org_coords, rot_coords, trans_coords, f0, f_tran) - dimer_cycles.append(dc) - - write_progress(geom0) - - if check_for_end_sign(): - break - logger.debug("") - print() - sys.stdout.flush() - print(f"@Did {tot_rot_force_evals} force evaluations in the rotation steps " - f"using the {opt_name_dict[rot_opt]} optimizer.") - tot_force_evals = tot_rot_force_evals + add_force_evals - print(f"@Used {add_force_evals} additional force evaluations for a total of " - f"{tot_force_evals} force evaluations.") - - dimer_results = DimerResult(dimer_cycles=dimer_cycles, - force_evals=tot_force_evals, - geom0=geom0, - converged=converged) - - return dimer_results diff --git a/deprecated/tsoptimizers/dimerv2.py b/deprecated/tsoptimizers/dimerv2.py deleted file mode 100644 index 7fb342271d..0000000000 --- a/deprecated/tsoptimizers/dimerv2.py +++ /dev/null @@ -1,231 +0,0 @@ -# [1] https://pubs.rsc.org/en/content/articlepdf/2002/cp/b108658h -# Farkas, 2001, GDIIS -# [2] https://aip.scitation.org/doi/abs/10.1063/1.4878944 -# Schaefer, 2014, DIIS + Dimer - -from collections import namedtuple -from functools import partial - -import numpy as np - -from pysisyphus.Geometry import Geometry -from pysisyphus.helpers import rms -from pysisyphus.optimizers.closures import lbfgs_closure_ as lbfgs_closure - - -RotResult = namedtuple("RotResult", - "geom1 geom2 N C coords", -) - - -def update_dimer_ends(geom0, geom1, geom2, N, R): - coords1 = geom0.coords + R*N - geom1.coords = coords1 - coords2 = geom0.coords - R*N - geom2.coords = coords2 - - -def get_dimer_ends(geom0, N, R, calc_getter): - dummy_coords = np.zeros_like(geom0.coords) - geom1 = Geometry(geom0.atoms, dummy_coords) - geom2 = Geometry(geom0.atoms, dummy_coords) - update_dimer_ends(geom0, geom1, geom2, N, R) - geom1.set_calculator(calc_getter()) - # We don't need a calculator on geom2 - - return geom1, geom2 - - -def get_f_rot(f0, f1, N): - frot = 2*(f1 - f0) - 2*(f1 - f0).dot(N)*N - return frot - - -class DIISError(Exception): - pass - - -def diis(error_vecs, coords, forces): - cycles = len(error_vecs) - - last_cr = None - for use_last in range(2, cycles+1): - A = np.zeros((use_last, use_last)) - # Start with the latest point and add previous points one by one - err_vecs = np.array(error_vecs[::-1])[:use_last] - # Scale error vector so that norm of smallest error vector is 1 - err_norms = np.linalg.norm(err_vecs, axis=1) - scale_factor = 1 / err_norms.min() - err_vecs *= scale_factor - - for i, e1 in enumerate(err_vecs): - for j in range(i, len(err_vecs)): - e2 = err_vecs[j] - A[i, j] = e1.dot(e2) - A[j, i] = A[i, j] - det = np.linalg.norm(A) - print(f"det(A)={det:.6f}") - cr = np.linalg.solve(A, np.ones(A.shape[0])) - if any(np.abs(cr) > 1e8): - break - last_cr = cr - if last_cr is None: - raise DIISError("DIIS failed!") - used_last = len(last_cr) - cs = last_cr / np.sum(last_cr) - print(f"DIIS with {used_last} vectors. Coeffs: ", cs) - last_coords = coords[::-1][:used_last] - last_error_vecs = error_vecs[::-1][:used_last] - last_forces = forces[::-1][:used_last] - - # Form linear combinations - coords = np.sum(cs[:,None]*last_coords, axis=0) - error = np.sum(cs[:,None]*last_error_vecs, axis=0) - force = np.sum(cs[:,None]*last_forces, axis=0) - return error, coords, force - - -def get_rot_optimizer(alpha=0.05): - cycle = 0 - error_vecs = list() - all_coords = list() - all_forces = list() - def get_rot_step(frot, coords1): - nonlocal cycle - - error_vecs.append(frot.copy()) - all_coords.append(coords1.copy()) - all_forces.append(frot.copy()) - - # Plain steepest descent step - step = alpha*frot - # Try DIIS from the second iteration onwards - if cycle > 0: - try: - error, coords1_, frot_ = diis(error_vecs, all_coords, all_forces) - # Inter-/extrapolated coords + step from inter-/extrapolated - # rotational forces. - new_diis_coords = coords1_ + alpha*frot_ - # Determine actual step as difference between the current coordinates - # and the DIIS coordinates. - step = new_diis_coords - coords1 - except DIISError: - pass - cycle += 1 - return step - return get_rot_step - - -def update_rotated_endpoints(geom0, geom1, geom2, geom1_step, R): - tmp_coords = geom1.coords + geom1_step - geom1.coords = tmp_coords - - N = geom1.coords - geom0.coords - N /= np.linalg.norm(N) - # Reconstrain dimer onto hypersphere - x1 = geom0.coords + R*N - x2 = geom0.coords - R*N - geom1.coords = x1 - geom2.coords = x2 - return geom1, geom2, N - - -def curvature(f0, f1, N, R): - C = 2*(f0-f1).dot(N)/(2*R) - return C - - -def rotate_dimer(geom0, geom1, geom2, N, R, f_thresh=2.5e-3, max_cycles=10, - alpha=0.05): - rot_optimizer = get_rot_optimizer(alpha=alpha) - f0 = geom0.forces - - coords = list() - for i in range(max_cycles): - f1 = geom1.forces - f_rot = get_f_rot(f0, f1, N) - f_rot_rms = np.linalg.norm(f_rot) - C = curvature(f0, f1, N, R) - print(f"Cycle {i:02d}: rms(f_rot)={f_rot_rms:.06f}, C={C: .06f}") - if f_rot_rms < f_thresh: - print("Converged") - break - step = rot_optimizer(f_rot, geom1.coords) - geom1, geom2, N = update_rotated_endpoints(geom0, geom1, geom2, step, R) - coords.append( - (geom1.coords.copy(), geom0.coords.copy(), geom2.coords.copy()) - ) - rot_result = RotResult( - geom1=geom1, - geom2=geom2, - N=N, - C=C, - coords=coords, - ) - return rot_result - - -def get_f_trans(geom0, N, C): - f0 = geom0.forces - - f_parallel = f0.dot(N)*N - if C > 0: - f_trans = -f_parallel - else: - f_trans = f0 - 2*f_parallel - - return f_trans - - -def restrict_step_length(_, step, max_length): - norm = np.linalg.norm(step) - if norm > max_length: - direction = step / norm - step = direction * max_length - return step - - -def dimer_method(geom0, N, R, calc_getter, max_cycles=50, f_thresh=1e-3, - max_step=0.3, - rot_kwargs=None, trans_kwargs=None): - if rot_kwargs is None: - rot_kwargs = {} - if trans_kwargs is None: - trans_kwargs = {} - - geom1, geom2 = get_dimer_ends(geom0, N, R, calc_getter) - coords = [(geom1.coords, geom0.coords, geom2.coords), ] - restrict_step = partial(restrict_step_length, max_length=max_step) - trans_optimizer = lbfgs_closure(lambda _, *args: get_f_trans(*args), - restrict_step=restrict_step) - for i in range(max_cycles): - f0 = geom0.forces - f0_rms = rms(f0) - print(f"{i:0d} rms(f0)={f0_rms:.6f}") - if f0_rms < f_thresh: - print("Converged!") - break - - # Rotation - rot_result = rotate_dimer(geom0, geom1, geom2, N, R, **rot_kwargs) - geom1, geom2, N, C, _ = rot_result - - # Translation - step, f_trans = trans_optimizer(geom0.coords, geom0, N, C) - new_coords0 = geom0.coords + step - geom0.coords = new_coords0 - - # Steepet descent - # f_trans = get_f_trans(geom0, N, C) - # alpha = 0.5 - # step = alpha * f_trans - # new_coords0 = geom0.coords + alpha*step - # geom0.coords = new_coords0 - - update_dimer_ends(geom0, geom1, geom2, N, R) - - coords.append( - (geom1.coords, geom0.coords, geom2.coords) - ) - # TODO: return dimerresults - return coords diff --git a/doc-requirements.txt b/doc-requirements.txt index 30f558aec1..22fb8cd829 100644 --- a/doc-requirements.txt +++ b/doc-requirements.txt @@ -1,146 +1,145 @@ # -# This file is autogenerated by pip-compile with Python 3.9 +# This file is autogenerated by pip-compile with Python 3.12 # by the following command: # # pip-compile --extra=doc --output-file=doc-requirements.txt setup.cfg # -alabaster==0.7.13 +alabaster==0.7.16 # via sphinx -autograd==1.5 +autograd==1.7.0 # via pysisyphus (setup.cfg) -babel==2.12.1 +babel==2.16.0 # via sphinx -bcrypt==4.0.1 +bcrypt==4.2.0 # via paramiko -certifi==2024.7.4 +certifi==2024.8.30 # via requests -cffi==1.15.1 +cffi==1.17.1 # via # cryptography # pynacl -charset-normalizer==3.1.0 +charset-normalizer==3.3.2 # via requests -click==8.1.3 +click==8.1.7 # via # dask # distributed -cloudpickle==2.2.1 +cloudpickle==3.0.0 # via # dask # distributed -contourpy==1.0.7 +contourpy==1.3.0 # via matplotlib cryptography==43.0.1 # via paramiko -cycler==0.11.0 +cycler==0.12.1 # via matplotlib -dask==2023.5.1 +dask==2024.8.2 # via # distributed # pysisyphus (setup.cfg) decorator==5.1.1 # via fabric -distributed==2023.5.1 +deprecated==1.2.14 + # via fabric +distributed==2024.8.2 # via pysisyphus (setup.cfg) -docutils==0.18.1 +docutils==0.20.1 # via # sphinx # sphinx-rtd-theme -fabric==3.1.0 +fabric==3.2.2 # via pysisyphus (setup.cfg) -fonttools==4.43.0 +fonttools==4.53.1 # via matplotlib -fsspec==2023.5.0 +fsspec==2024.9.0 # via dask -future==0.18.3 - # via autograd -h5py==3.8.0 +h5py==3.11.0 # via pysisyphus (setup.cfg) -idna==3.7 +idna==3.8 # via requests imagesize==1.4.1 # via sphinx -importlib-metadata==6.6.0 - # via - # dask - # sphinx -importlib-resources==5.12.0 - # via matplotlib -invoke==2.1.2 +invoke==2.2.0 # via fabric jinja2==3.1.4 # via # distributed # pysisyphus (setup.cfg) # sphinx -joblib==1.2.0 - # via - # pysisyphus (setup.cfg) - # scikit-learn -kiwisolver==1.4.4 +joblib==1.4.2 + # via scikit-learn +kiwisolver==1.4.7 # via matplotlib +llvmlite==0.43.0 + # via numba locket==1.0.0 # via # distributed # partd -markupsafe==2.1.2 +markupsafe==2.1.5 # via jinja2 -matplotlib==3.7.1 +matplotlib==3.9.2 # via pysisyphus (setup.cfg) mpmath==1.3.0 # via sympy -msgpack==1.0.5 +msgpack==1.0.8 # via distributed -natsort==8.3.1 +natsort==8.4.0 # via pysisyphus (setup.cfg) -numpy==1.24.3 +networkx==3.3 + # via pysisyphus (setup.cfg) +numba==0.60.0 + # via pysisyphus (setup.cfg) +numpy==2.0.2 # via # autograd # contourpy # h5py # matplotlib + # numba # pysisyphus (setup.cfg) # rmsd # scikit-learn # scipy -packaging==23.1 +packaging==24.1 # via # dask # distributed # matplotlib # sphinx -paramiko==3.4.0 +paramiko==3.4.1 # via fabric -partd==1.4.0 +partd==1.4.2 # via dask -pillow==10.3.0 +pillow==10.4.0 # via matplotlib -psutil==5.9.5 +psutil==6.0.0 # via # distributed # pysisyphus (setup.cfg) -pycparser==2.21 +pycparser==2.22 # via cffi -pygments==2.15.1 +pygments==2.18.0 # via sphinx pynacl==1.5.0 # via paramiko -pyparsing==3.0.9 +pyparsing==3.1.4 # via matplotlib -python-dateutil==2.8.2 +python-dateutil==2.9.0.post0 # via matplotlib -pyyaml==6.0 +pyyaml==6.0.2 # via # dask # distributed # pysisyphus (setup.cfg) -requests==2.32.0 +requests==2.32.3 # via sphinx rmsd==1.5.1 # via pysisyphus (setup.cfg) -scikit-learn==1.5.0 +scikit-learn==1.5.1 # via pysisyphus (setup.cfg) -scipy==1.10.1 +scipy==1.14.1 # via # pysisyphus (setup.cfg) # rmsd @@ -151,37 +150,37 @@ snowballstemmer==2.2.0 # via sphinx sortedcontainers==2.4.0 # via distributed -sphinx==6.2.1 +sphinx==7.4.7 # via # pysisyphus (setup.cfg) # sphinx-autodoc-typehints # sphinx-rtd-theme # sphinxcontrib-jquery -sphinx-autodoc-typehints==1.23.0 +sphinx-autodoc-typehints==2.3.0 # via pysisyphus (setup.cfg) -sphinx-rtd-theme==1.2.1 +sphinx-rtd-theme==2.0.0 # via pysisyphus (setup.cfg) -sphinxcontrib-applehelp==1.0.4 +sphinxcontrib-applehelp==2.0.0 # via sphinx -sphinxcontrib-devhelp==1.0.2 +sphinxcontrib-devhelp==2.0.0 # via sphinx -sphinxcontrib-htmlhelp==2.0.1 +sphinxcontrib-htmlhelp==2.1.0 # via sphinx sphinxcontrib-jquery==4.1 # via sphinx-rtd-theme sphinxcontrib-jsmath==1.0.1 # via sphinx -sphinxcontrib-qthelp==1.0.3 +sphinxcontrib-qthelp==2.0.0 # via sphinx -sphinxcontrib-serializinghtml==1.1.5 +sphinxcontrib-serializinghtml==2.0.0 # via sphinx -sympy==1.12 +sympy==1.13.2 # via pysisyphus (setup.cfg) -tblib==1.7.0 +tblib==3.0.0 # via distributed -threadpoolctl==3.1.0 +threadpoolctl==3.5.0 # via scikit-learn -toolz==0.12.0 +toolz==0.12.1 # via # dask # distributed @@ -192,9 +191,7 @@ urllib3==2.2.2 # via # distributed # requests +wrapt==1.16.0 + # via deprecated zict==3.0.0 # via distributed -zipp==3.19.1 - # via - # importlib-metadata - # importlib-resources diff --git a/docs/chainofstates.rst b/docs/chainofstates.rst index e20296b50e..c0d2e579a4 100644 --- a/docs/chainofstates.rst +++ b/docs/chainofstates.rst @@ -43,6 +43,26 @@ along the tangent is projected out and replaced by an artificial spring force. In principle, optimizing NEBs should be easier as there is no reparametrization and more sophisticated optimizers beyond SD can and should be employed. +Parallelization +=============== +Parallel calculation of multiple images is possible using the +`Dask.distributed `_ package. The easiest +way is to include `cluster: True` in the `cos:` section (see below). A documented +example is found below: + +.. literalinclude :: ../examples/complex/13_orca_parallel_neb/13_orca_parallel_neb.yaml + :language: yaml + :caption: + +Parallelization via Dask should work for most calculators that are executed via +the `subprocess` module as external processes, but probably not calculators +like `PySCF`. + +In order to use/watch the nice dashboard provided by dask, please install a +recent version of bokeh (`python -m pip install bokeh`). By default, the +dashboard is available under `127.0.0.1:8787 <127.0.0.1:8787>`_ when the cluster +is started by pysisyphus. + General remarks =============== @@ -76,6 +96,10 @@ that the user may want to modify when running a GSM optimization. perp_thresh: 0.05 # Threshold for growing new frontier nodes. reparam_every: 2 # Reparametrize every n-th cycle when NOT fully grown. reparam_every_full: 3 # Reparametrize every n-th cycle when fully grown. + cluster: False # Parallelize COS calculations using Dask cluster + cluster_kwargs: None # Dict; additional arguments for LocalCluster, + # e.g., LocalCluster + scheduler: None # Address to (external) Dask scheduler opt: type: string # Optimizer for GrowingString stop_in_when_full: -1 # Stop string optimization N cycles after fully grown @@ -152,7 +176,7 @@ Further examples for COS optimizations from `.yaml` input can be found `here `_. General advice for COS optimizations -=================================== +==================================== - Start from optimized geometries or use the `preopt:` key in the YAML input. - Consider fixing the initial and final images `fix_ends: True` @@ -211,7 +235,7 @@ Simple Zero-Temperature String :show-inheritance: Growing Chain Of States base class -========================== +================================== Base class for growing chain of state methods @@ -220,7 +244,7 @@ Base class for growing chain of state methods :undoc-members: Growing Chain Of State Methods -====================== +============================== Growing String Method ------------------------- diff --git a/docs/diabatization.rst b/docs/diabatization.rst index 50ad98c24c..1e9321160f 100644 --- a/docs/diabatization.rst +++ b/docs/diabatization.rst @@ -60,7 +60,7 @@ The related diabatic states :math:`\Xi_A` and :math:`\Xi_B` are .. math:: \begin{align} \Xi_A &= \cos(\theta) \Psi_1 + \sin(\theta) \Psi_2 \\ - \Xi_B &= -\sin(\theta) \Psi_1 - \cos(\theta) \Psi_2 \\ + \Xi_B &= -\sin(\theta) \Psi_1 + \cos(\theta) \Psi_2 \\ \end{align} ~ . Similarly, diabatic expectation values are calculated as linear combination of diff --git a/examples/calc/04_pyscf_methane_perf/04_pyscf_methane_perf.yaml b/examples/calc/04_pyscf_methane_perf/04_pyscf_methane_perf.yaml index c900a3e807..b200568b10 100644 --- a/examples/calc/04_pyscf_methane_perf/04_pyscf_methane_perf.yaml +++ b/examples/calc/04_pyscf_methane_perf/04_pyscf_methane_perf.yaml @@ -2,9 +2,9 @@ geom: fn: lib:methane.xyz calc: type: pyscf - basis: def2svp + basis: def2tzvpp perf: - pal_range: [1, 4, 1] + pal_range: [1, 5, 1] mems: 1000 - repeat: 2 + repeat: 3 kind: hessian diff --git a/examples/complex/07_diels_alder_xtb_bfgs_preopt_neb_tsopt_irc/07_diels_alder_xtb_bfgs_preopt_neb_tsopt_irc.yaml b/examples/complex/07_diels_alder_xtb_bfgs_preopt_neb_tsopt_irc/07_diels_alder_xtb_bfgs_preopt_neb_tsopt_irc.yaml index 210929eaee..9e153c2986 100644 --- a/examples/complex/07_diels_alder_xtb_bfgs_preopt_neb_tsopt_irc/07_diels_alder_xtb_bfgs_preopt_neb_tsopt_irc.yaml +++ b/examples/complex/07_diels_alder_xtb_bfgs_preopt_neb_tsopt_irc/07_diels_alder_xtb_bfgs_preopt_neb_tsopt_irc.yaml @@ -1,32 +1,35 @@ geom: - type: cart + type: cartesian fn: diels_alder.trj calc: type: xtb charge: 0 mult: 1 - pal: 4 + pal: 6 + acc: 0.01 preopt: - max_cycles: 5 interpol: type: redund - between: 10 + between: 12 cos: type: neb + climb: True + climb_lanczos: True + cluster: True opt: - type: bfgs - align: False - rms_force: 0.01 - max_step: 0.2 + type: lbfgs + align: True + rms_force_only: True tsopt: - type: rsirfo do_hess: True - max_cycles: 75 - thresh: gau_tight - hessian_recalc: 10 + thresh: gau + hessian_recalc: 5 irc: - type: eulerpc rms_grad_thresh: 0.0005 + hessian_recalc: 5 +endopt: + fragments: True + do_hess: True assert: ts_geom.energy: -17.81225938 - ts_opt.cur_cycle: 12 + #ts_opt.cur_cycle: 12 diff --git a/examples/complex/13_orca_parallel_neb/13_orca_parallel_neb.yaml b/examples/complex/13_orca_parallel_neb/13_orca_parallel_neb.yaml new file mode 100644 index 0000000000..a1f159ebff --- /dev/null +++ b/examples/complex/13_orca_parallel_neb/13_orca_parallel_neb.yaml @@ -0,0 +1,51 @@ +geom: + type: cartesian + fn: lib:ala_dipeptide_iso_b3lyp_631gd_10_images.trj +calc: + type: orca + keywords: hf-3c tightscf + charge: 0 + mult: 1 + # Setting pal to a number > 1 is still a good idea, even though + # the value is modified internally in parallel COS runs. As some + # follow-up calculations, e.g., TS-optimizations are serial it is + # important to pick a sensible value for 'pal'. + # + # As my notebook has 6 physical cores I set 'pal' to 6. + pal: 6 +cos: + type: neb + # Setting only 'cluster: True' without any 'cluster_kwargs' uses all + # avaiable physical cores by default (logical cores, e.g., from + # Hyperthreading are disabled by default). + cluster: True + # The number of avaiable threads can be controlled by setting 'threads_per_worker' + # to an appropriate value. When 'cluster_kwargs' with some arguments is given + # 'cluster' is automatically set to True. + # + # It is probably a good idea to set 'threads_per_worker' to the number of actual + # physical cores to be utilized. + # + # As there are 1+8+1 images in total and only 8 are moving I pick 4 + # workers, so all images can be treated in two batches. In the first + # cycle two additional calculations (first and last image) will be done. + cluster_kwargs: + n_workers: 1 + threads_per_worker: 4 + # pysisyphus uses resources to manage the different tasks, so the appropriate + # number of CPU resources per worker must be given here. This should be the same + # number as used in threads_per_worker. + # + # As mentioned above; just setting cluster: True should be enough in most of the + # cases. + resources: + CPU: 4 + # Alternatively, the address to an external scheduler not managed by + # pysisyphus can be provided. If an address is provided, this takes + # precedence and pysisyphus won't create an internal cluster. + # scheduler: 127.0.0.1:39271 +opt: + type: lbfgs + align: True + # Production calculations require many more cycles. + max_cycles: 5 diff --git a/examples/opt/28_ascii_art/28_ascii_art.yaml b/examples/opt/28_ascii_art/28_ascii_art.yaml new file mode 100644 index 0000000000..fbf97f2f87 --- /dev/null +++ b/examples/opt/28_ascii_art/28_ascii_art.yaml @@ -0,0 +1,39 @@ +geom: + type: cartesian + fn: | + 24 + + C -2.06618165 -2.74160982 -1.96941073 + C -2.28186249 -3.64676791 -0.92075636 + C -0.94776144 -2.30373215 0.66394015 + H -2.48998513 -2.89960782 -2.96752643 + H -1.92695459 -4.16280963 1.17613845 + C 2.30970598 2.43402365 2.08685122 + C -0.87543028 -1.59465211 -0.48604750 + C -1.75104361 -3.45163322 0.36118106 + C -1.28036998 -1.56028325 -1.77776585 + H -2.88880011 -4.53559748 -1.10938862 + H -0.50888543 -2.09126806 1.64646231 + H -1.08730013 -0.80860797 -2.55286599 + N -2.74283034 0.98932764 0.05937817 + C 2.88528390 2.58951054 -0.29600751 + C 1.52181731 2.41931865 -0.69648637 + H 1.17407471 2.41791003 -1.73746321 + N -2.68085569 1.76558417 -0.71914772 + H 0.13640927 2.13072836 2.56039380 + H 2.60116497 2.44588559 3.14293335 + C 0.81256974 2.28347692 0.44960128 + H 4.29761755 2.74235557 1.32141211 + H 3.62466495 2.72423025 -1.09318111 + C 0.91792617 2.25657695 1.79957857 + C 3.24702432 2.59764212 1.05817590 +calc: + type: orca + keywords: hf-3c + pal: 2 + charge: 2 + mult: 1 +opt: + # Watch resulting file in another window + # watch -n 1 cat geom_ascii + dump_ascii: True diff --git a/flake8.ini b/flake8.ini new file mode 100644 index 0000000000..28719fa257 --- /dev/null +++ b/flake8.ini @@ -0,0 +1,2 @@ +[flake8] +exclude = pysisyphus/wavefunction/ints,pysisyphus/wavefunction/ints_numba diff --git a/pyproject.toml b/pyproject.toml index f49c588775..fb574e4815 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,6 +5,10 @@ build-backend = "setuptools.build_meta" [tool.setuptools_scm] write_to = "pysisyphus/version.py" +[tool.ruff] + [tool.ruff.lint] +# E741: ambiguous variable name +ignore = ["E741"] extend-select = ["NPY201"] preview = true diff --git a/pyrightconfig.json b/pyrightconfig.json index eff8f24402..3756284db2 100644 --- a/pyrightconfig.json +++ b/pyrightconfig.json @@ -4,6 +4,7 @@ ], "exclude": [ "pysisyphus/wavefunction/ints", + "pysisyphus/wavefunction/ints_numba", "pysisyphus/intcoords/derivatives.py", "pysisyphus/intcoords/mp_derivatives.py" ], diff --git a/pysisyphus/Geometry.py b/pysisyphus/Geometry.py index 79eff2daac..ca693a8b56 100644 --- a/pysisyphus/Geometry.py +++ b/pysisyphus/Geometry.py @@ -1,10 +1,12 @@ from collections import Counter, namedtuple import copy import itertools as it +from pathlib import Path import re import subprocess import tempfile import sys +import warnings import h5py import numpy as np @@ -18,14 +20,19 @@ except ModuleNotFoundError: pass -from pysisyphus import logger from pysisyphus.config import p_DEFAULT, T_DEFAULT from pysisyphus.constants import BOHR2ANG +from pysisyphus.exceptions import DifferentAtomOrdering + +from pysisyphus.wavefunction.excited_states import norm_ci_coeffs +from pysisyphus.hessian_proj import get_hessian_projector, inertia_tensor +from pysisyphus.linalg import are_collinear from pysisyphus.elem_data import ( - MASS_DICT, - ISOTOPE_DICT, ATOMIC_NUMBERS, COVALENT_RADII as CR, + INV_ATOMIC_NUMBERS, + ISOTOPE_DICT, + MASS_DICT, VDW_RADII as VDWR, ) from pysisyphus.helpers_pure import ( @@ -52,107 +59,24 @@ from pysisyphus.intcoords.helpers import get_tangent from pysisyphus.intcoords.setup import BOND_FACTOR from pysisyphus.intcoords.setup_fast import find_bonds +from pysisyphus.plot_ascii import plot_wrapper from pysisyphus.xyzloader import make_xyz_str -def inertia_tensor(coords3d, masses): - """Inertita tensor. - - | x² xy xz | - (x y z)^T . (x y z) = | xy y² yz | - | xz yz z² | - """ - x, y, z = coords3d.T - squares = np.sum(coords3d**2 * masses[:, None], axis=0) - I_xx = squares[1] + squares[2] - I_yy = squares[0] + squares[2] - I_zz = squares[0] + squares[1] - I_xy = -np.sum(masses * x * y) - I_xz = -np.sum(masses * x * z) - I_yz = -np.sum(masses * y * z) - I = np.array(((I_xx, I_xy, I_xz), (I_xy, I_yy, I_yz), (I_xz, I_yz, I_zz))) - return I - - -def get_trans_rot_vectors(cart_coords, masses, rot_thresh=1e-6): - """Vectors describing translation and rotation. - - These vectors are used for the Eckart projection by constructing - a projector from them. - - See Martin J. Field - A Pratcial Introduction to the simulation - of Molecular Systems, 2007, Cambridge University Press, Eq. (8.23), - (8.24) and (8.26) for the actual projection. - - See also https://chemistry.stackexchange.com/a/74923. - - Parameters - ---------- - cart_coords : np.array, 1d, shape (3 * atoms.size, ) - Atomic masses in amu. - masses : iterable, 1d, shape (atoms.size, ) - Atomic masses in amu. - - Returns - ------- - ortho_vecs : np.array(6, 3*atoms.size) - 2d array containing row vectors describing translations - and rotations. - """ - - coords3d = np.reshape(cart_coords, (-1, 3)) - total_mass = masses.sum() - com = 1 / total_mass * np.sum(coords3d * masses[:, None], axis=0) - coords3d_centered = coords3d - com[None, :] - - I = inertia_tensor(coords3d, masses) - _, Iv = np.linalg.eigh(I) - Iv = Iv.T - - masses_rep = np.repeat(masses, 3) - sqrt_masses = np.sqrt(masses_rep) - num = len(masses) - - def get_trans_vecs(): - """Mass-weighted unit vectors of the three cartesian axes.""" - - for vec in ((1, 0, 0), (0, 1, 0), (0, 0, 1)): - _ = sqrt_masses * np.tile(vec, num) - yield _ / np.linalg.norm(_) - - def get_rot_vecs(): - """As done in geomeTRIC.""" - - rot_vecs = np.zeros((3, cart_coords.size)) - # p_vecs = Iv.dot(coords3d_centered.T).T - for i in range(masses.size): - p_vec = Iv.dot(coords3d_centered[i]) - for ix in range(3): - rot_vecs[0, 3 * i + ix] = Iv[2, ix] * p_vec[1] - Iv[1, ix] * p_vec[2] - rot_vecs[1, 3 * i + ix] = Iv[2, ix] * p_vec[0] - Iv[0, ix] * p_vec[2] - rot_vecs[2, 3 * i + ix] = Iv[0, ix] * p_vec[1] - Iv[1, ix] * p_vec[0] - rot_vecs *= sqrt_masses[None, :] - return rot_vecs - - trans_vecs = list(get_trans_vecs()) - rot_vecs = np.array(get_rot_vecs()) - # Drop vectors with vanishing norms - rot_vecs = rot_vecs[np.linalg.norm(rot_vecs, axis=1) > rot_thresh] - tr_vecs = np.concatenate((trans_vecs, rot_vecs), axis=0) - tr_vecs = np.linalg.qr(tr_vecs.T)[0].T - return tr_vecs - - -def get_trans_rot_projector(cart_coords, masses, full=False): - tr_vecs = get_trans_rot_vectors(cart_coords, masses=masses) - U, s, _ = np.linalg.svd(tr_vecs.T) - if full: - P = np.eye(cart_coords.size) - for tr_vec in tr_vecs: - P -= np.outer(tr_vec, tr_vec) - else: - P = U[:, s.size :].T - return P +def normalize_atoms(atoms) -> tuple[str]: + atomic_numbers = set(INV_ATOMIC_NUMBERS.keys()) + _atoms = list() + for atom in atoms: + try: + atom_int = int(atom) + if atom_int in atomic_numbers: + atom = INV_ATOMIC_NUMBERS[atom_int] + except ValueError: + pass + # Was atom.capitalize() before ... + atom = atom.lower() + _atoms.append(atom) + return tuple(_atoms) class Geometry: @@ -177,6 +101,8 @@ def __init__( coord_kwargs=None, isotopes=None, freeze_atoms=None, + remove_com=False, + remove_centroid=False, comment="", name="", ): @@ -208,12 +134,16 @@ def __init__( Given a float, this float will be directly used as mass. freeze_atoms : iterable of integers Specifies which atoms should remain fixed at their initial positions. + remove_com : bool, optional + Move center of mass to the origin. + remove_centroid : bool, optional + Move centroid to the origin. comment : str, optional Comment string. name : str, optional Verbose name of the geometry, e.g. methanal or water. Used for printing """ - self.atoms = tuple([atom.capitalize() for atom in atoms]) + self.atoms = normalize_atoms(atoms) # self._coords always holds cartesian coordinates. self._coords = np.array(coords, dtype=float).flatten() assert self._coords.size == (3 * len(self.atoms)), ( @@ -236,10 +166,17 @@ def __init__( elif type(freeze_atoms) is str: freeze_atoms = full_expand(freeze_atoms) self.freeze_atoms = np.array(freeze_atoms, dtype=int) + self.remove_com = bool(remove_com) + self.remove_centroid = bool(remove_centroid) self.comment = comment self.name = name self._masses = None + if self.remove_com: + self.coords3d = self.coords3d - self.center_of_mass[None, :] + if self.remove_centroid: + self.coords3d = self.coords3d - self.centroid[None, :] + self._energy = None self._forces = None self._hessian = None @@ -293,8 +230,10 @@ def moving_atoms_jmol(self): @property def sum_formula(self): - unique_atoms = sorted(set(self.atoms)) - counter = Counter(self.atoms) + atoms = self.atoms + atoms = [atom.capitalize() for atom in atoms] + unique_atoms = sorted(set(atoms)) + counter = Counter(atoms) atoms = list() num_strs = list() @@ -375,6 +314,10 @@ def __add__(self, other): coords = np.concatenate((self.cart_coords, other.cart_coords)) return Geometry(atoms, coords) + @property + def is_linear(self): + return are_collinear(self.coords3d) + def atom_xyz_iter(self): return iter(zip(self.atoms, self.coords3d)) @@ -481,11 +424,9 @@ def layers(self): return layers def del_atoms(self, inds, **kwargs): - atoms = [atom for i, atom in enumerate(self.atoms) if not (i in inds)] + atoms = [atom for i, atom in enumerate(self.atoms) if i not in inds] c3d = self.coords3d - coords3d = np.array( - [c3d[i] for i, _ in enumerate(self.atoms) if not (i in inds)] - ) + coords3d = np.array([c3d[i] for i, _ in enumerate(self.atoms) if i not in inds]) return Geometry(atoms, coords3d.flatten(), **kwargs) def set_calculator(self, calculator, clear=True): @@ -551,7 +492,7 @@ def set_coords(self, coords, cartesian=False, update_constraints=False): coords = np.array(coords).flatten() # Do Internal->Cartesian backtransformation if internal coordinates are used. - if self.internal: + if hasattr(self, "internal") and self.internal: # When internal coordinates are employed it may happen, that the underlying # Cartesian coordinates are updated, e.g. from the IPIServer calculator, which # may yield different internal coordinates. @@ -616,7 +557,12 @@ def set_coords(self, coords, cartesian=False, update_constraints=False): bend to a linear bend and its complement. Currently the default.""" - coord_kwargs["define_prims"] = valid_typed_prims + coord_kwargs.update( + { + "define_prims": valid_typed_prims, + "constrain_prims": self.internal.constrain_prims, + } + ) self.internal = coord_class(self.atoms, coords3d, **coord_kwargs) self._coords = coords3d.flatten() @@ -727,7 +673,7 @@ def comment(self, new_comment): self._comment = new_comment @property - def masses(self): + def masses(self) -> np.ndarray: if self._masses is None: # Lookup tabuled masses in internal database masses = np.array([MASS_DICT[atom.lower()] for atom in self.atoms]) @@ -883,9 +829,9 @@ def standard_orientation(self): if aligned: break - def reparametrize(self): + def reparametrize(self, energy, forces): # Currently, self.calculator.get_coords is only implemented by the - # IPIPServer, but it is deactivated there. + # IPServer, but it is deactivated there. try: # TODO: allow skipping the update results = self.calculator.get_coords(self.atoms, self.cart_coords) @@ -919,6 +865,10 @@ def energy(self, energy): """ self._energy = energy + @property + def has_energy(self): + return self._energy is not None + @property def all_energies(self): """Return energies of all states that were calculated. @@ -926,10 +876,16 @@ def all_energies(self): This will also set self.energy, which may NOT be the ground state, but the state correspondig to the 'root' attribute of the calculator.""" if self._all_energies is None: - results = self.calculator.get_energy(self.atoms, self._coords) + results = self.calculator.get_all_energies(self.atoms, self._coords) self.set_results(results) return self._all_energies + def get_root_energy(self, root): + return self.all_energies[root] + + def has_all_energies(self): + return self._all_energies is not None + @all_energies.setter def all_energies(self, all_energies): """Internal wrapper for setting all energies. @@ -980,6 +936,10 @@ def forces(self, forces): assert forces.shape == self.cart_coords.shape self._forces = forces + @property + def has_forces(self): + return self._forces is not None + @property def cart_gradient(self): return -self.cart_forces @@ -1048,6 +1008,9 @@ def hessian(self): return self.internal.transform_hessian(hessian, int_gradient) return hessian + def has_hessian(self): + return self._hessian is not None + # @hessian.setter # def hessian(self, hessian): # """Internal wrapper for setting the hessian.""" @@ -1102,13 +1065,23 @@ def set_h5_hessian(self, fn): if valid: self.cart_hessian = hessian - def get_normal_modes(self, cart_hessian=None, full=False): + def get_normal_modes( + self, cart_hessian=None, cart_gradient=None, proj_gradient=False, full=False + ): """Normal mode wavenumbers, eigenvalues and Cartesian displacements Hessian.""" if cart_hessian is None: cart_hessian = self.cart_hessian + if not proj_gradient: + mw_gradient = None + elif cart_gradient is None: + mw_gradient = self.mw_gradient + else: + mw_gradient = cart_gradient / np.sqrt(self.masses_rep) mw_hessian = self.mass_weigh_hessian(cart_hessian) - proj_hessian, P = self.eckart_projection(mw_hessian, return_P=True, full=full) + proj_hessian, P = self.eckart_projection( + mw_hessian, return_P=True, mw_gradient=mw_gradient, full=full + ) eigvals, eigvecs = np.linalg.eigh(proj_hessian) mw_cart_displs = P.T.dot(eigvecs) cart_displs = self.mm_sqrt_inv.dot(mw_cart_displs) @@ -1136,9 +1109,9 @@ def get_thermoanalysis( mult = self.calculator.mult except AttributeError: mult = 1 - logger.debug( + warnings.warn( "Multiplicity for electronic entropy could not be determined! " - f"Using 2S+1 = {mult}." + f"Falling back to Using 2S+1 = {mult}." ) thermo_dict = { @@ -1157,14 +1130,30 @@ def get_thermoanalysis( return thermo def get_trans_rot_projector(self, full=False): - return get_trans_rot_projector(self.cart_coords, masses=self.masses, full=full) + warnings.warn( + "'Geometry.get_trans_rot_projector()' is deprecated. Please use " + "'Geometry.get_hessian_projector() instead.", + DeprecationWarning, + ) + return get_hessian_projector(self.cart_coords, masses=self.masses, full=full) - def eckart_projection(self, mw_hessian, return_P=False, full=False): + def get_hessian_projector(self, cart_gradient=None, full=False): + return get_hessian_projector( + self.cart_coords, masses=self.masses, cart_gradient=cart_gradient, full=full + ) + + def eckart_projection( + self, mw_hessian, return_P=False, mw_gradient=None, full=False + ): # Must not project analytical 2d potentials. if self.is_analytical_2d: return mw_hessian - P = self.get_trans_rot_projector(full=full) + if mw_gradient is not None: + cart_gradient = mw_gradient * np.sqrt(self.masses_rep) + else: + cart_gradient = None + P = self.get_hessian_projector(cart_gradient=cart_gradient, full=full) proj_hessian = P.dot(mw_hessian).dot(P.T) # Projection seems to slightly break symmetry (sometimes?). Resymmetrize. proj_hessian = (proj_hessian + proj_hessian.T) / 2 @@ -1173,8 +1162,13 @@ def eckart_projection(self, mw_hessian, return_P=False, full=False): else: return proj_hessian + def calc_energy(self): + """Force energy calculation at the current coordinates.""" + results = self.calculator.get_energy(self.atoms, self.cart_coords) + self.set_results(results) + def calc_energy_and_forces(self): - """Force a calculation of the current energy and forces.""" + """Force energy and forces calculation at the current coordinates.""" results = self.calculator.get_forces(self.atoms, self.cart_coords) self.set_results(results) @@ -1232,6 +1226,68 @@ def calc_double_ao_overlap(self, geom2): def zero_frozen_forces(self, cart_forces): cart_forces.reshape(-1, 3)[self.freeze_atoms] = 0.0 + def calc_wavefunction(self, **prepare_kwargs): + # TODO: support wf (kw)-args? + results = self.calculator.get_wavefunction( + self.atoms, self.cart_coords, **prepare_kwargs + ) + self.set_results(results) + return results + + @property + def wavefunction(self): + if self._wavefunction is None: + self.calc_wavefunction() + return self._wavefunction + + @wavefunction.setter + def wavefunction(self, wavefunction): + self._wavefunction = wavefunction + + @property + def td_1tdms(self): + """1-particle transition density matrices from TD-DFT/TDA. + + Returns list of Xa, Ya, Xb and Yb in MO basis.""" + if self._td_1tdms is None: + self.all_energies + return self._td_1tdms + + @td_1tdms.setter + def td_1tdms(self, td_1tdms): + self._td_1tdms = td_1tdms + + def calc_relaxed_density(self, root, **prepare_kwargs): + """Calculate a relaxed excited state density via an ES gradient calculation. + + The question is, if this method should set the wavefunction property + at the current Geometry. On one hand, staying in pure python w/o numba + the wavefunction sanity-check can become costly, even though it shouldn't + be. + On the other hand, setting the wavefunction would ensure consistency + between the levels of theory used for density and wavefunction. + + For now, calculating an ES density does not set a wavefunction on the + Geometry, whereas requesting the relaxed density for the GS does. + + TODO: add flag that allows setting the wavefunction (WF). Then, + calculators should also include the WF in their results.""" + if root == 0: + results = self.calc_wavefunction(**prepare_kwargs) + wf = results["wavefunction"] + density = wf.get_relaxed_density(0) + else: + results = self.calculator.get_relaxed_density( + self.atoms, self.cart_coords, root, **prepare_kwargs + ) + # Don't set density on Geometry + density = results.pop("density") + self.set_results(results) + results["density"] = density + if self.internal: + results["forces"] = self.internal.transform_forces(results["forces"]) + return results + def clear(self): """Reset the object state.""" @@ -1242,6 +1298,8 @@ def clear(self): self.true_forces = None self.true_hessian = None self._all_energies = None + self._wavefunction = None + self._td_1tdms = None def set_results(self, results): """Save the results from a dictionary. @@ -1263,14 +1321,20 @@ def set_results(self, results): "true_hessian": "true_hessian", # Overlap calculator; includes excited states "all_energies": "all_energies", + "td_1tdms": "td_1tdms", + # Wavefunction related + "wavefunction": "wavefunction", } - for key in results: + for key, value in results.items(): # Zero forces of frozen atoms if key == "forces": - self.zero_frozen_forces(results[key]) + self.zero_frozen_forces(value) + elif key == "td_1tdms": + value = norm_ci_coeffs(*value) + results[key] = value - setattr(self, trans[key], results[key]) + setattr(self, trans[key], value) self.results = results def as_xyz(self, comment="", atoms=None, cart_coords=None): @@ -1295,7 +1359,9 @@ def as_xyz(self, comment="", atoms=None, cart_coords=None): cart_coords = self._coords cart_coords = cart_coords.copy() cart_coords *= BOHR2ANG - if comment == "": + if comment is None: + comment = "" + elif comment == "": comment = self.comment return make_xyz_str(atoms, cart_coords.reshape((-1, 3)), comment) @@ -1306,7 +1372,18 @@ def dump_xyz(self, fn, cart_coords=None, **kwargs): with open(fn, "w") as handle: handle.write(self.as_xyz(cart_coords=cart_coords, **kwargs)) - def get_subgeom(self, indices, coord_type="cart", sort=False): + def dump_trj(self, fn, trj_cart_coords, **kwargs): + fn = Path(fn).with_suffix(".trj") + xyzs = list() + for cart_coords in trj_cart_coords: + xyz = self.as_xyz(cart_coords=cart_coords, **kwargs) + xyzs.append(xyz) + trj = "\n".join(xyzs) + with open(fn, "w") as handle: + handle.write(trj) + return fn + + def get_subgeom(self, indices, coord_type="cart", sort=False, cart_coords=None): """Return a Geometry containing a subset of the current Geometry. Parameters @@ -1315,17 +1392,23 @@ def get_subgeom(self, indices, coord_type="cart", sort=False): Atomic indices that the define the subset of the current Geometry. coord_type : str, ("cart", "redund"), optional Coordinate system of the new Geometry. + cart_coords + Optional 1d array of Cartesian coordinates of shape (3*natoms, ). Returns ------- sub_geom : Geometry Subset of the current Geometry. """ + if cart_coords is not None: + coords3d = cart_coords.reshape(-1, 3) + else: + coords3d = self.coords3d if sort: indices = sorted(indices) ind_list = list(indices) sub_atoms = [self.atoms[i] for i in ind_list] - sub_coords = self.coords3d[ind_list] + sub_coords = coords3d[ind_list] sub_geom = Geometry(sub_atoms, sub_coords.flatten(), coord_type=coord_type) return sub_geom @@ -1333,10 +1416,15 @@ def get_subgeom_without(self, indices, **kwargs): with_indices = [ind for ind, _ in enumerate(self.atoms) if ind not in indices] return self.get_subgeom(with_indices, **kwargs) - def rmsd(self, geom): - return rmsd.kabsch_rmsd( - self.coords3d - self.centroid, geom.coords3d - geom.centroid - ) + def rmsd(self, geom, align=True): + if not self.atoms == geom.atoms: + raise DifferentAtomOrdering + if align: + return rmsd.kabsch_rmsd( + self.coords3d - self.centroid, geom.coords3d - geom.centroid + ) + else: + return np.sqrt(np.mean((self.cart_coords - geom.cart_coords) ** 2)) def as_g98_list(self): """Returns data for fake Gaussian98 standard orientation output. @@ -1362,14 +1450,22 @@ def tmp_xyz_handle(self, atoms=None, cart_coords=None): tmp_xyz.flush() return tmp_xyz - def jmol(self, atoms=None, cart_coords=None): - """Show geometry in jmol.""" + def jmol(self, atoms=None, cart_coords=None, stdin=None, jmol_cmd="jmol"): + """Show geometry in jmol. + + TODO: read jmol command from .pysisyphusrc ?!""" tmp_xyz = self.tmp_xyz_handle(atoms, cart_coords) - jmol_cmd = "jmol" + cmd = [jmol_cmd, tmp_xyz.name] + if stdin is not None: + cmd = cmd + ["-s", "-"] try: - subprocess.run([jmol_cmd, tmp_xyz.name]) + subprocess.run( + cmd, + input=stdin, + text=True, + ) except FileNotFoundError: - print(f"'{jmol_cmd}' seems not to be on your path!") + print(f"'{jmol_cmd}' does not seem to be on your $PATH!") tmp_xyz.close() def modes3d(self): @@ -1383,7 +1479,7 @@ def modes3d(self): subprocess.run(f"modes3d.py {tmp_xyz.name}{bonds_str}", shell=True) tmp_xyz.close() - def as_ase_atoms(self): + def as_ase_atoms(self, vacuum=None): try: import ase except ImportError: @@ -1391,7 +1487,10 @@ def as_ase_atoms(self): return None # ASE coordinates are in Angstrom - atoms = ase.Atoms(symbols=self.atoms, positions=self.coords3d * BOHR2ANG) + capital_atoms = map(lambda s: s.capitalize(), self.atoms) + atoms = ase.Atoms(symbols=capital_atoms, positions=self.coords3d * BOHR2ANG) + if vacuum is not None: + atoms.center(vacuum=vacuum) if self.calculator is not None: from pysisyphus.calculators import FakeASE @@ -1400,6 +1499,12 @@ def as_ase_atoms(self): atoms.set_calculator(ase_calc) return atoms + def as_ascii_art(self) -> str: + """ASCII-art representation of the Geometry. + + Using code from gpaw. Requires an ase installation.""" + return plot_wrapper(self) + def get_restart_info(self): # Geometry restart information restart_info = { diff --git a/pysisyphus/MOCoeffs.py b/pysisyphus/MOCoeffs.py new file mode 100644 index 0000000000..0608e9a0ff --- /dev/null +++ b/pysisyphus/MOCoeffs.py @@ -0,0 +1,200 @@ +import dataclasses +import math +from typing import Optional, Tuple + +import matplotlib.pyplot as plt +import numpy as np + +from pysisyphus.constants import AU2EV + + +def occ_from_occs(occs): + occ = occs.sum() + iocc = round(occ) + # Don't allow fractional total occupations + assert math.isclose(float(iocc), occ, abs_tol=1e-10) + return iocc + + +def filter_ens( + ens: np.ndarray, en_homo: float, below_homo: float, above_homo: float +) -> Tuple[np.ndarray, np.ndarray]: + below_thresh = en_homo - below_homo + above_thresh = en_homo + above_homo + ens_between = list() + indices_between = list() + for i, en in enumerate(ens): + if below_thresh <= en <= above_thresh: + ens_between.append(en) + indices_between.append(i) + ens_between = np.array(ens_between) + indices_between = np.array(indices_between, dtype=int) + return ens_between, indices_between + + +@dataclasses.dataclass +class MOCoeffs: + # Ca/Cb: 2d array of floats w/ shape (naos, nmos) containing MO coefficients + # ensa/ensb: 1d array of floats w/ shape (nmos, ) containing MO energies + # occsa/occs: 1d array of floats w/ shape (nmos, ) containing MO occupation numbers + Ca: np.ndarray + ensa: np.ndarray + occsa: np.ndarray + # Beta electron/MO quantities are optional, as they are not present in restricted + # calculations. When only alpha quantities are given all beta quantities will be + # derived/copied from them. + Cb: Optional[np.ndarray] = None + ensb: Optional[np.ndarray] = None + occsb: Optional[np.ndarray] = None + + def __post_init__(self): + self._restricted = self.Cb is None + + # All beta quantities must be given together + if self.unrestricted: + assert self.ensb is not None and self.occsb is not None + else: + self.Cb = self.Ca.copy() + self.ensb = self.ensa.copy() + self.occsa = self.occsa / 2.0 + self.occsb = self.occsa.copy() + + self._occa = occ_from_occs(self.occsa) + self._occb = occ_from_occs(self.occsb) + + @property + def restricted(self) -> bool: + return self._restricted + + @property + def unrestricted(self) -> bool: + return not self.restricted + + @property + def occa(self) -> int: + return self._occa + + @property + def occb(self) -> int: + return self._occb + + def _homo(self, occ): + return occ - 1 if occ else None + + @property + def homoa(self) -> Optional[int]: + return self._homo(self.occa) + + @property + def homob(self) -> Optional[int]: + return self._homo(self.occb) + + @property + def lumoa(self): + return self.occa + + @property + def lumob(self): + return self.occb + + def _virt_inds(self, occs): + return np.arange(occs.size)[occs == 0.0] + + @property + def virt_indsa(self): + return self._virt_inds(self.occsa) + + @property + def virt_indsb(self): + return self._virt_inds(self.occsb) + + def swap_inplace( + self, + C: np.ndarray, + ens: np.ndarray, + occs: np.ndarray, + ind1: int, + ind2: int, + swap_energies: bool = True, + swap_occs: bool = True, + ): + """Swap a pair of MO coeffs and energies inplace.""" + tmp = C[:, ind1].copy() + C[:, ind1] = C[:, ind2] + C[:, ind2] = tmp + if swap_energies: + ens[ind1], ens[ind2] = ens[ind2], ens[ind1] + if swap_occs: + occs[ind1], occs[ind2] = occs[ind2], occs[ind1] + + def swap_mos(self, alpha_pairs, beta_pairs=None, **kwargs): + if beta_pairs is None: + beta_pairs = list() + + new_kwargs = dataclasses.asdict(self) + + Ca = new_kwargs["Ca"] + ensa = new_kwargs["ensa"] + occsa = new_kwargs["occsa"] + for ind1, ind2 in alpha_pairs: + self.swap_inplace(Ca, ensa, occsa, ind1, ind2, **kwargs) + + Cb = new_kwargs["Cb"] + ensb = new_kwargs["ensb"] + occsb = new_kwargs["occsa"] + for ind1, ind2 in beta_pairs: + self.swap_inplace(Cb, ensb, occsb, ind1, ind2, **kwargs) + return MOCoeffs(**new_kwargs) + + def plot_mo_energies(self, below_homo=0.5, above_homo=0.5, show=False): + ensa = self.ensa + homoa = self.homoa + en_homoa = ensa[homoa] + ensb = self.ensb + homob = self.homob + en_homob = ensa[homob] + + ensa, indsa = filter_ens(ensa, en_homoa, below_homo, above_homo) + ensb, indsb = filter_ens(ensb, en_homob, below_homo, above_homo) + occsa = self.occsa[indsa] + occsb = self.occsb[indsb] + + def colors(occs): + return ["red" if occ else "blue" for occ in occs] + + colorsa = colors(occsa) + colorsb = colors(occsb) + + en_homoa = en_homoa * AU2EV + en_homob = en_homob * AU2EV + ensa = ensa * AU2EV + ensb = ensb * AU2EV + xa = np.ones_like(ensa) + xb = np.ones_like(ensb) + 1 + + fig, ax = plt.subplots() + + def annot(xs, ens, inds): + for x, en, ind in zip(xs, ens, inds): + text = str(ind) + xy = (x + 0.125, en) + ax.annotate(text, xy) + + kwargs = { + "s": 200, + "marker": "_", + "zorder": 3, + } + ax.scatter(xa, ensa, c=colorsa, label="α", **kwargs) + ax.scatter(xb, ensb, c=colorsb, label="β", **kwargs) + annot(xa, ensa, indsa) + annot(xb, ensb, indsb) + ax.axhline(en_homoa, c="k", ls="--", label="HOMO α") + ax.axhline(en_homob, c="k", ls="--", label="HOMO β") + ax.legend() + ax.set_ylabel("E / eV") + ax.set_xlim(0, 3) + fig.tight_layout() + if show: + plt.show() + return fig, ax diff --git a/pysisyphus/TableFormatter.py b/pysisyphus/TableFormatter.py deleted file mode 100644 index 6ffee3be45..0000000000 --- a/pysisyphus/TableFormatter.py +++ /dev/null @@ -1,63 +0,0 @@ -import random - -import numpy as np - -class TableFormatter: - - def __init__(self, header, fmts, min_width=7, space=3): - self.min_width = min_width + (space-1) - self.space = space - - # Get lengths of header strings - widths = np.array([len(h) for h in header]) - # Expand entries smaller than min_widht to min_width - smaller_indices = widths < min_width - widths[smaller_indices] = min_width - self.widths = widths - - # Construct header - #header_fmts = ["{:" + "{}".format(width) + "s}" - # for width in self.widths] - header_fmts = self.min_width_fmts() - self._header = self.join_format(header_fmts, header) - self._header += "\n" + (self.space*" ").join( - ["-"*width for width in self.widths] - ) - - # Modify fmts to consinder min_widths - self.fmts = self.min_width_fmts(fmts) - - def min_width_fmts(self, raw_fmts=None): - if not raw_fmts: - raw_fmts = ["s" for _ in self.widths] - return ["{:>" + "{}".format(width) + fmt + "}" - for width, fmt in zip(self.widths, raw_fmts)] - - def join_format(self, fmts, lst): - """Format a given iterable lst with formats given in the iterable - lst and return the joined items of the formatted list.""" - return (self.space*" ").join( - [fmt.format(item) for fmt, item in zip(fmts, lst)] - ) - - @property - def header(self): - return self._header - - def line(self, *args): - #formatted = " "[" - return self.join_format(self.fmts, args) - -def run(): - header = "# |dx| |tangent|".split() - fmts = ["d", ".2E", ".3E"] - min_width = 10 - tp = TableFormatter(header, fmts, min_width) - print(tp.header) - for i in range(10): - dx = random.random() - tangent = random.random() - print(tp.line(i, dx, tangent)) - -if __name__ == "__main__": - run() diff --git a/pysisyphus/TablePrinter.py b/pysisyphus/TablePrinter.py index 3490f15d0d..bae71f23e5 100644 --- a/pysisyphus/TablePrinter.py +++ b/pysisyphus/TablePrinter.py @@ -1,5 +1,73 @@ +import logging import textwrap +import pyparsing as pp + + +lit = pp.Literal +start = lit("{:") +end = lit("}") +alignment = lit(">") | lit("<") | lit("^") +sign = lit("+") | lit("-") | lit(" ") +int_num = pp.Word(pp.nums).set_parse_action(lambda s, l, t: int(t[0])) +width = int_num("width") +precision = pp.Suppress(lit(".")) + int_num("precision") +type_ = ( + lit("s") + | lit("b") + | lit("c") + | lit("d") + | lit("o") + | lit("x") + | lit("X") + | lit("e") + | lit("E") + | lit("f") + | lit("F") + | lit("g") + | lit("G") + | lit("n") + | lit("%") +) + +fparser = ( + start + + pp.Optional(alignment)("alignment") + + pp.Optional(sign)("sign") + + pp.Optional(width) + + pp.Optional(precision) + + pp.Optional(type_)("type") + + end +) + + +def parse_fstring(fstr) -> dict: + result = fparser.parse_string(fstr) + return result.as_dict() + + +def res_to_fstring(res: dict): + alignment = res.get("alignment", "") + sign = res.get("sign", "") + width = res.get("width", "") + precision = res.get("precision", "") + if precision: + precision = f".{precision}" + type_ = res["type"] + return f"{{:{alignment}{sign}{width}{precision}{type_}}}" + + +def center(string, width): + length = len(string) + whitespace = width - length + if whitespace >= 2: + before = " " * (whitespace // 2) + after = before + centered = f"{before}{string}{after}" + else: + centered = string + return centered + class TablePrinter: def __init__( @@ -8,50 +76,111 @@ def __init__( col_fmts, width=12, sub_underline=True, - shift_left=0, - fmts_update=None, mark="*", + offset=8, + logger=None, + level=logging.INFO, ): self.header = header self.col_fmts = col_fmts self.width = width self.sub_underline = sub_underline - self.shift_left = shift_left - if fmts_update is None: - fmts_update = {} - self.fmts_update = fmts_update self.mark = mark + self.logger = logger + self.level = level - w = str(self.width - 1) + assert len(header) == len(col_fmts), ( + f"Number of {len(header)} fields does not match the number of " + f"column formats {len(col_fmts)}" + ) + w = str(self.width) # Shortcut + whalf = str(self.width // 2) self.fmts = { - "int": "{:>" + w + "d}", - "float": "{: >" + w + ".6f}", - "float_short": "{: >" + w + ".2f}", "str": "{:>" + w + "s}", "mark": "{:>1s}", + # + "int": "{:>" + w + "d}", + "int_short": "{:>" + whalf + "d}", + "int3": "{: >3d}", + # + "float": "{: >" + w + ".6f}", + "float_short": "{: >" + whalf + ".3f}", + # + "complex_tdm": "{: >" + w + ".4f}", + "complex_short": "{: >" + w + ".2f}", } - self.fmts.update(fmts_update) if self.sub_underline: self.header = [h.replace("_", " ") for h in self.header] - self.header_str = " ".join([h.rjust(self.width) for h in self.header]) + # Determine alignments and widths from given formats + fmts = list() + alignments = list() + widths = list() + for i, col_fmt in enumerate(self.col_fmts): + # First, try to look up the format in our prepopulated + # dictionary. + try: + fmt = self.fmts[col_fmt] + # Second, if not found in the dict, we assume that the + # string is a valid f-string. + except KeyError: + fmt = col_fmt + # In any case, we parse the given string to determine + # alignment and width. + res = parse_fstring(fmt) + alignment = res.get("alignment", "<") + alignments.append(alignment) + # Check if header is longer than the desired width. + header_width = len(self.header[i]) + width = max(res.get("width", self.width), header_width) + # Update the fstring with the (possibly) new width and reconstruct it + res["width"] = width + fmt = res_to_fstring(res) + widths.append(width) + fmts.append(fmt) + + # TODO: Check if header fields are longer than the given width. + # If so, update the widths in the formats. + mark_fmt = self.fmts["mark"] - self.conv_str = " ".join([self.fmts[fmt] + mark_fmt for fmt in self.col_fmts]) - h0_len = len(self.header[0]) - self.offset = self.width - h0_len - self.prefix = " " * (self.offset - self.shift_left) - self.sep = ( - self.prefix - + "-" * (len(self.header_str) - self.width + h0_len) - + "-" * abs(self.shift_left) - ) + # self.conv_str will be used to render the given fields in a row + self.conv_str = " ".join([fmt + mark_fmt for fmt in fmts]) + + # Distance from the line start + self.offset = offset + # Whitespace that is prepended on every row + self.prefix = " " * self.offset + # Length of a given line w/o offset/prefix + self.line_length = sum(widths) + len(widths) + len(widths) - 1 + # Separator string + self.sep = self.prefix + "-" * self.line_length + + header_fmts = list() + for alignment, width in zip(alignments, widths): + hfmt = "{:" + alignment + str(width) + "}" + header_fmts.append(hfmt) + # Join with 2 spaces as we also have the mark field + header_fmt = " ".join(header_fmts) + self.header_str = self.prefix + header_fmt.format(*self.header) + + @property + def nfields(self) -> int: + return len(self.header) + + def _print(self, msg, level=None): + if level is None: + level = self.level + if self.logger: + self.logger.log(level, msg) + else: + print(msg) def print_sep(self): - print(self.sep) + self._print(self.sep) def print_header(self, with_sep=True): - print(self.header_str) + self._print(self.header_str) if with_sep: self.print_sep() @@ -62,7 +191,8 @@ def print_row(self, args, marks=None): for arg, to_mark in zip(args, marks): marked_args.append(arg) marked_args.append(self.mark if to_mark else " ") - print(self.conv_str.format(*marked_args)) + row = self.prefix + self.conv_str.format(*marked_args) + self._print(row) def print(self, *args, **kwargs): text = " ".join([str(a) for a in args]) @@ -71,4 +201,17 @@ def print(self, *args, **kwargs): except KeyError: level = 0 level_prefix = " " * level - print(textwrap.indent(text, self.prefix + level_prefix)) + self._print(textwrap.indent(text, self.prefix + level_prefix)) + + def print_rows(self, all_args, marks=None, first_n=None): + assert len(all_args) == self.nfields + nrows = len(all_args[0]) + # Verify that provided args all have the same length + assert all([len(arg) == nrows for arg in all_args]) + # Update the number of rows that will be printed + if first_n is not None: + nrows = min(nrows, first_n) + + for i in range(nrows): + args_i = [arg[i] for arg in all_args] + self.print_row(args_i, marks=marks) diff --git a/pysisyphus/__init__.py b/pysisyphus/__init__.py index b045447068..e6abe7a046 100644 --- a/pysisyphus/__init__.py +++ b/pysisyphus/__init__.py @@ -3,10 +3,12 @@ from .version import version as __version__ -logger = logging.getLogger("pysisyphus") -logger.setLevel(logging.DEBUG) + +logger = logging.getLogger("pysis") +logger.setLevel(logging.INFO) file_handler = logging.FileHandler("pysisyphus.log", mode="w", delay=True) +file_handler.setLevel(logging.INFO) logger.addHandler(file_handler) stdout_handler = logging.StreamHandler(sys.stdout) diff --git a/pysisyphus/benchmarks/__init__.py b/pysisyphus/benchmarks/__init__.py index 0081fa7676..71a555b1fb 100644 --- a/pysisyphus/benchmarks/__init__.py +++ b/pysisyphus/benchmarks/__init__.py @@ -1 +1 @@ -from pysisyphus.benchmarks.Benchmark import Benchmark +from pysisyphus.benchmarks.Benchmark import Benchmark as Benchmark diff --git a/pysisyphus/calculators/AFIR.py b/pysisyphus/calculators/AFIR.py index e53e3bbcb5..8c43142c5b 100644 --- a/pysisyphus/calculators/AFIR.py +++ b/pysisyphus/calculators/AFIR.py @@ -13,11 +13,11 @@ from pysisyphus.calculators.Calculator import Calculator from pysisyphus.elem_data import COVALENT_RADII +from pysisyphus.finite_diffs import finite_difference_hessian from pysisyphus.Geometry import Geometry from pysisyphus.helpers import complete_fragments from pysisyphus.helpers_pure import log from pysisyphus.io.hdf5 import get_h5_group, resize_h5_group -from pysisyphus.linalg import finite_difference_hessian def get_data_model(atoms, max_cycles): @@ -27,13 +27,13 @@ def get_data_model(atoms, max_cycles): _3d = (max_cycles, coord_size, coord_size) data_model = { - "cart_coords": _2d, - "energy": _1d, - "forces": _2d, - "hessian": _3d, - "true_energy": _1d, - "true_forces": _2d, - "true_hessian": _3d, + "cart_coords": (_2d, np.float64), + "energy": (_1d, np.float64), + "forces": (_2d, np.float64), + "hessian": (_3d, np.float64), + "true_energy": (_1d, np.float64), + "true_forces": (_2d, np.float64), + "true_hessian": (_3d, np.float64), } return data_model diff --git a/pysisyphus/calculators/AnaPotBase.py b/pysisyphus/calculators/AnaPotBase.py index 48b37afc1d..f8acf6b892 100644 --- a/pysisyphus/calculators/AnaPotBase.py +++ b/pysisyphus/calculators/AnaPotBase.py @@ -1,7 +1,6 @@ from matplotlib import cm import matplotlib.animation as animation import matplotlib.pyplot as plt -from mpl_toolkits.mplot3d import Axes3D import numpy as np from sympy import symbols, diff, lambdify, sympify @@ -22,13 +21,14 @@ def __init__( use_sympify=True, minima=None, saddles=None, + **kwargs, ): - super().__init__() + super().__init__(**kwargs) self.V_str = V_str self.scale = scale self.xlim = xlim self.ylim = ylim - if levels is not None: + if levels is not None and not (type(levels) == int): levels = levels * self.scale self.levels = levels if minima is None: @@ -71,8 +71,9 @@ def __init__( self.fig = None self.ax = None - def get_energy(self, atoms, coords): - self.energy_calcs += 1 + def get_energy(self, atoms, coords, increase_counter=True): + if increase_counter: + self.energy_calcs += 1 x, y, z = coords energy = self.scale * self.V(x, y) return { @@ -89,7 +90,9 @@ def get_forces(self, atoms, coords): results = { "forces": forces, } - results.update(self.get_energy(atoms, coords)) + # Don't increase the energy counter to mimic the fact, that a QM force + # calculation also produces an energy. + results.update(self.get_energy(atoms, coords, increase_counter=False)) return results def get_hessian(self, atoms, coords): @@ -104,13 +107,17 @@ def get_hessian(self, atoms, coords): results = { "hessian": hessian, } - results.update(self.get_energy(atoms, coords)) + # Don't increase the energy counter to mimic the fact, that a QM Hessian + # calculation also produces an energy. + results.update(self.get_energy(atoms, coords, increase_counter=False)) return results def statistics(self): return ( - f"Energy calculations: {self.energy_calcs}, Force calculations: " - f"{self.forces_calcs}, Hessian calculations: {self.hessian_calcs}" + # f"Energy calculations: {self.energy_calcs}, Force calculations: " + # f"{self.forces_calcs}, Hessian calculations: {self.hessian_calcs}" + f"Energy evals: {self.energy_calcs}, force evals: {self.forces_calcs}, " + f"Hessian evals: {self.hessian_calcs}" ) def plot(self, levels=None, show=False, **figkwargs): @@ -153,10 +160,13 @@ def plot3d( nan_above=None, init_view=None, colorbar=False, + computed_zorder=True, **figkwargs, ): self.fig = plt.figure(**figkwargs) - self.ax = self.fig.add_subplot(111, projection="3d") + self.ax = self.fig.add_subplot( + 111, projection="3d", computed_zorder=computed_zorder + ) x = np.linspace(*self.xlim, resolution) y = np.linspace(*self.ylim, resolution) X, Y = np.meshgrid(x, y) @@ -277,6 +287,28 @@ def func(frame): if show: plt.show() + def anim_cos_coords(self, coords, interval=50, show=False): + self.plot() + nsteps = len(coords) + steps = range(nsteps) + coords = np.array(coords).reshape(nsteps, -1, 3) + # Drop z-coordinate + coords = coords[:, :, :2] + lines, *_ = self.ax.plot(*coords[0].T, "o-") + + def func(frame): + curx, cury = coords[frame].T + lines.set_xdata(curx) + lines.set_ydata(cury) + self.ax.set_title(f"Frame {frame}") + + self.animation = animation.FuncAnimation( + self.fig, func, frames=steps, interval=interval + ) + + if show: + plt.show() + @classmethod def get_geom( cls, coords, atoms=("X",), V_str=None, calc_kwargs=None, geom_kwargs=None @@ -294,6 +326,7 @@ def get_geom( def get_path(self, num, minima_inds=None): between = num - 2 + assert between >= 0 inds = 0, 1 if minima_inds is not None: diff --git a/pysisyphus/calculators/Calculator.py b/pysisyphus/calculators/Calculator.py index 212a56d4cc..f67835d197 100644 --- a/pysisyphus/calculators/Calculator.py +++ b/pysisyphus/calculators/Calculator.py @@ -15,8 +15,13 @@ from pysisyphus import helpers_pure from pysisyphus.config import get_cmd, OUT_DIR_DEFAULT from pysisyphus.constants import BOHR2ANG +from pysisyphus.exceptions import ( + CalculationFailedException, + RunAfterCalculationFailedException, +) +from pysisyphus.finite_diffs import finite_difference_hessian_mp from pysisyphus.helpers import geom_loader -from pysisyphus.linalg import finite_difference_hessian +from pysisyphus.wavefunction import Wavefunction KeepKind = Enum("KeepKind", ["ALL", "LATEST", "NONE"]) @@ -37,7 +42,6 @@ def __post_init__(self): class Calculator: - conf_key = None _set_plans = [] @@ -150,13 +154,17 @@ def __init__( if force_num_hess: self.force_num_hessian() + self.run_call_counts = dict() + def get_cmd(self, key="cmd"): assert self.conf_key, "'conf_key'-attribute is missing for this calculator!" try: return get_cmd(section=self.conf_key, key=key, use_defaults=True) except KeyError: - logger.debug(f"Failed to load key '{key}' from section '{self.conf_key}'!") + logger.warning( + f"Failed to load key '{key}' from section '{self.conf_key}'!" + ) @classmethod def geom_from_fn(cls, fn, **kwargs): @@ -199,6 +207,8 @@ def get_hessian(self, atoms, coords, **prepare_kwargs): def get_num_hessian(self, atoms, coords, **prepare_kwargs): self.log("Calculating numerical Hessian.") + # Calculate energy here so a converged wavefunction is available for the + # parallel Hessian calculation. results = self.get_energy(atoms, coords, **prepare_kwargs) def grad_func(coords): @@ -206,20 +216,21 @@ def grad_func(coords): gradient = -results["forces"] return gradient - def callback(i, j): - self.log(f"Displacement {j} of coordinate {i}") + def callback(i): + self.log(f"Displacement {i}") _num_hess_kwargs = { "step_size": 0.005, - # Central difference by default + # Central difference with two displacements by default "acc": 2, } _num_hess_kwargs.update(self.num_hess_kwargs) - fd_hessian = finite_difference_hessian( + fd_hessian = finite_difference_hessian_mp( + atoms, coords, - grad_func, - callback=callback, + self, + prepare_kwargs=prepare_kwargs, **_num_hess_kwargs, ) results["hessian"] = fd_hessian @@ -238,6 +249,20 @@ def restore_org_hessian(self): self.get_hessian = self._org_get_hessian self.hessian_kind = HessKind["ORG"] + def get_wavefunction(self, atoms, coords, **prepare_kwargs): + """Meant to be extended.""" + raise Exception("Not implemented!") + + def get_relaxed_density(self, atoms, coords, root, **prepare_kwargs): + """Meant to be extended.""" + raise Exception("Not implemented!") + + def load_wavefunction_from_file(self, fn, **kwargs): + return Wavefunction.from_file(fn, **kwargs) + + def get_stored_wavefunction(self, **kwargs): + raise Exception("Not implemented!") + def make_fn(self, name, counter=None, return_str=False): """Make a full filename. @@ -457,6 +482,10 @@ def run( like the energy, a forces vector and/or excited state energies from TDDFT. """ + # print( + # f"@ In Calculator.run({calc=}) w/ {self.base_name=}, {self.calc_number=: >2d}, " + # f"{self.calc_counter=: >05d}" + # ) self.backup_dir = None path = self.prepare(inp) @@ -504,6 +533,8 @@ def run( # Do at least one cycle. When retries are disabled retry_calc == 0 # and range(0+1) will result in one cycle added_retry_args = False + self.run_call_counts.setdefault(calc, 0) + self.run_call_counts[calc] += 1 for retry in range(self.retry_calc + 1): result = subprocess.Popen( args, @@ -514,11 +545,15 @@ def run( shell=shell, ) result.communicate() + returncode = result.returncode + nonzero_return = returncode != 0 try: normal_termination = False # Calling check_termination may result in an exception and # normal_termination will stay at False - normal_termination = self.check_termination(tmp_out_fn) + normal_termination = ( + self.retry_calc == 0 + ) or self.check_termination(tmp_out_fn) # Method check_termination may not be implemented, so we will always # do only one try. except AttributeError: @@ -552,6 +587,14 @@ def run( # Parse results for desired quantities try: + # Don't try to parse calculation results when the calculated already + # returned an nonzero error code. + if nonzero_return: + raise CalculationFailedException( + msg=f"Calculation in '{path}' returned with code {returncode}!", + path=path, + ) + if run_after: self.run_after(path) parser_kwargs = {} if parser_kwargs is None else parser_kwargs @@ -571,7 +614,9 @@ def run( f"Copied contents of\n\t'{path}'\nto\n\t'{backup_dir}'.\n" "Consider checking the log files there.\n" ) - raise err + raise RunAfterCalculationFailedException( + "Postprocessing calculation failed!", err + ) finally: if (not hold) and self.clean_after: self.clean(path) diff --git a/pysisyphus/calculators/Composite.py b/pysisyphus/calculators/Composite.py index 0c3b3a3aa6..7fe98d03bd 100644 --- a/pysisyphus/calculators/Composite.py +++ b/pysisyphus/calculators/Composite.py @@ -1,10 +1,12 @@ +# [1] https://doi.org/10.1021/acscentsci.3c01403 +# Exploring Chemical Space Using Ab Initio Hyperreactor Dynamics +# Stan-Bernhardt, Glinkina, Hulm, Ochsenfeld, 2024 + import numpy as np import sympy as sym -# from sympy import sympify, lambdify, - from pysisyphus.calculators.Calculator import Calculator - +from pysisyphus.calculators import pyscf_lazy from pysisyphus.calculators import ORCA, Turbomole, DFTD4 @@ -13,12 +15,6 @@ "orca": ORCA.ORCA, "turbomole": Turbomole.Turbomole, } -try: - from pysisyphus.calculators import PySCF - - CALC_CLASSES["pyscf"] = PySCF.PySCF -except (ModuleNotFoundError, OSError): - pass class Composite(Calculator): @@ -46,6 +42,8 @@ def __init__( # Don't modify original dict kwargs = kwargs.copy() type_ = kwargs.pop("type") + if type_ == "pyscf": + pyscf_lazy.add_pyscf_to_dict(CALC_CLASSES) calc_kwargs.update(**kwargs) calc_cls = CALC_CLASSES[type_] keys_calcs[key] = calc_cls(**calc_kwargs) @@ -56,40 +54,112 @@ def __init__( self.remove_translation = remove_translation # The energies are just numbers that we can easily substitute in - self.energy_expr = sym.sympify(self.final) - # The forces/Hessians are matrices that we can't just easily substitute in. - self.arr_args = sym.symbols(" ".join(self.keys_calcs.keys())) - self.arr_expr = sym.lambdify(self.arr_args, self.energy_expr) + expr = sym.sympify(self.final) + free_symbs = expr.free_symbols + symb_names = {symb.name for symb in free_symbs} + keys = list(self.keys_calcs.keys()) + if not symb_names == set(keys): + unknown_symbs = free_symbs - keys + raise Exception(f"Found unknown symbol(s): {unknown_symbs} in 'final'!") + + x = sym.symbols("x", real=True) + key_funcs = {symb: sym.Function(symb.name)(x) for symb in free_symbs} + + # Map between key string and sympy symbol + key_symbs = {} + for key in keys: + for fsymb in free_symbs: + if fsymb.name == key: + key_symbs[key] = fsymb - def get_final_energy(self, energies): - return float(self.energy_expr.subs(energies).evalf()) + expr_x = expr.subs(key_funcs) + # Take derivatives w.r.t. coordinates + dexpr_x = sym.diff(expr_x, x) # Gradient + d2expr_x = sym.diff(expr_x, x, x) # Hessian + + """ + Energies will always be present and gradients/derivatives never appear in the energy + expression. The gradient expression may depend on the energy, but it will always be + available when a gradient is calculated. + Special care is be needed when dealing with the Hessian, as it may also depend on the + gradient, that is not necessarily calculated when calculating a Hessian. So we create + a list of first derivatives that appear in the Hessian (derivs_in_d2). + """ + derivs = {} + derivs2 = {} + derivs_in_d2 = {} + # Inverted dict 'key_funcs' + func_subs = {value: key for key, value in key_funcs.items()} + deriv_subs = {} + deriv2_subs = {} + deriv_args = list() + deriv2_deriv_args = list() + deriv2_args = list() + for key in keys: + symb = key_symbs[key] + name = symb.name + func = key_funcs[symb] + deriv = sym.Derivative(func, x) + deriv2 = sym.Derivative(func, (x, 2)) + # Squared derivatives appearing in the expression actually correspond to Hessians + d2expr_x = d2expr_x.subs(deriv**2, deriv2) + derivs[symb] = deriv + derivs2[symb] = deriv2 + deriv_symb = sym.symbols(f"{name}_deriv", real=True) + deriv_args.append(deriv_symb.name) + deriv2_symb = sym.symbols(f"{name}_deriv2", real=True) + deriv2_args.append(deriv2_symb.name) + + # Check if gradient appears in Hessian expressions + if has_deriv := d2expr_x.has(deriv): + derivs_in_d2[symb] = has_deriv + deriv2_deriv_args.append(deriv_symb.name) + deriv_subs[deriv] = deriv_symb + deriv2_subs[deriv2] = deriv2_symb + dexpr = dexpr_x.subs(deriv_subs).subs(func_subs) + d2expr = d2expr_x.subs(deriv2_subs).subs(deriv_subs).subs(func_subs) + assert ( + len(deriv2_deriv_args) == 0 + ), "Hessian expressions that depend on the first derivative are not yet supported!" + + deriv_args = keys + deriv_args + deriv2_args = keys + deriv2_deriv_args + deriv2_args + + # Setup function that will be used to evaluate the energy and its derivatives + # from the different calculators. + self.energy_func = sym.lambdify(keys, expr) + self.grad_func = sym.lambdify(deriv_args, dexpr) + self.hessian_func = sym.lambdify(deriv2_args, d2expr) def get_energy(self, atoms, coords, **prepare_kwargs): - energies = {} + energy_kwargs = {} for key, calc in self.keys_calcs.items(): energy = calc.get_energy(atoms, coords, **prepare_kwargs)["energy"] - energies[key] = energy + energy_kwargs[key] = energy - final_energy = self.get_final_energy(energies) + final_energy = self.energy_func(**energy_kwargs) results = { "energy": final_energy, } return results def get_forces(self, atoms, coords, **prepare_kwargs): - energies = {} - forces = {} + energy_kwargs = {} + deriv_kwargs = dict() for key, calc in self.keys_calcs.items(): results = calc.get_forces(atoms, coords, **prepare_kwargs) - energies[key] = results["energy"] - forces[key] = results["forces"] + energy_kwargs[key] = results["energy"] + deriv_kwargs[key] = results["energy"] + deriv_kwargs[key + "_deriv"] = -results["forces"] keys = self.keys_calcs.keys() for key in keys: - self.log(f"|forces_{key}|={np.linalg.norm(forces[key]):.6f}") + self.log( + f"|forces_{key}|={np.linalg.norm(deriv_kwargs[key + '_deriv']):.6f}" + ) self.log("") - final_energy = self.get_final_energy(energies) - final_forces = self.arr_expr(**forces) + final_energy = self.energy_func(**energy_kwargs) + final_forces = -self.grad_func(**deriv_kwargs) # Remove overall translation if self.remove_translation: @@ -103,15 +173,16 @@ def get_forces(self, atoms, coords, **prepare_kwargs): return results def get_hessian(self, atoms, coords, **prepare_kwargs): - energies = {} - hessians = {} + energy_kwargs = {} + deriv2_kwargs = {} for key, calc in self.keys_calcs.items(): results = calc.get_hessian(atoms, coords, **prepare_kwargs) - energies[key] = results["energy"] - hessians[key] = results["hessian"] + energy_kwargs[key] = results["energy"] + deriv2_kwargs[key] = results["energy"] + deriv2_kwargs[key + "_deriv2"] = results["hessian"] - final_energy = self.get_final_energy(energies) - final_hessian = self.arr_expr(**hessians) + final_energy = self.energy_func(**energy_kwargs) + final_hessian = self.hessian_func(**deriv2_kwargs) results = { "energy": final_energy, @@ -121,3 +192,97 @@ def get_hessian(self, atoms, coords, **prepare_kwargs): def run_calculation(self, atoms, coords, **prepare_kwargs): return self.get_energy(atoms, coords, **prepare_kwargs) + + +def get_boosted_composite_calc( + calc: Calculator, dV: str, E: float, **calc_kwargs +) -> Composite: + """ + Get Composite calculator, boosting the potential energy below a given threshold. + + See eq. (4) in [1]. + + Parameters + ---------- + calc + Base calculator. + dV + Boost expression. + E + Threshold energy, below which the potential energy is boosted. + + Returns + ------- + gamd_comp_calc + Calculator that boosts the potential energy when it falls below + the given threshold. + """ + V = "base" # Original, unmodified energy + + # Boost expression that depends on the calculated potential energy. + # + # Energy is boosted (V + dV) when V is below given threshold E. + # The original unmodified energy V is returned otherwise (V >= E). + pot_str = f"Piecewise(({V}, {V} >= {E}), ({V} + {dV}, {V} < {E}))" + calc_kwargs = calc_kwargs.copy() + _calc_kwargs = { + "keys_calcs": { + "base": calc, + }, + "final": pot_str, + } + calc_kwargs.update(_calc_kwargs) + return Composite(**calc_kwargs) + + +def get_aMD_composite_calc( + calc: Calculator, alpha: float, E: float, **calc_kwargs +) -> Composite: + """ + Get Composite calculator for accelerated molecular dynamics. + + See Table 1 in [1]. + + Parameters + ---------- + calc + Base calculator. + alpha + Bias strength. + E + Threshold energy, below which the potential energy is boosted. + + Returns + ------- + amd_comp_calc + Boosting calculator for aMD. + """ + dEV = f"{E} - base" + dV = f"{dEV}**2 / ({alpha} + {dEV})" + return get_boosted_composite_calc(calc, dV=dV, E=E, **calc_kwargs) + + +def get_GaMD_composite_calc( + calc: Calculator, k: float, E: float, **calc_kwargs +) -> Composite: + """ + Get Composite calculator for Gaussian-accelerated molecular dynamics. + + See Table 1 in [1]. + + Parameters + ---------- + calc + Base calculator. + k + Force constant. + E + Threshold energy, below which the potential energy is boosted. + + Returns + ------- + gamd_comp_calc + Boosting calculator for GaMD. + """ + dV = f"0.5 * {k} * ({E} - base)**2" # GaMD energy boost/correction + return get_boosted_composite_calc(calc, dV=dV, E=E, **calc_kwargs) diff --git a/pysisyphus/calculators/ConicalIntersection.py b/pysisyphus/calculators/ConicalIntersection.py index 1617a67b92..3de41b7aa1 100644 --- a/pysisyphus/calculators/ConicalIntersection.py +++ b/pysisyphus/calculators/ConicalIntersection.py @@ -5,8 +5,9 @@ from copy import deepcopy -from dataclasses import dataclass +import dataclasses from math import sqrt +from typing import Optional import numpy as np @@ -20,7 +21,7 @@ def update_y(x, x_prev, y_prev): y_prev_x = y_prev.dot(x) x_prev_x = x_prev.dot(x) - y = (y_prev_x * x_prev - x_prev_x * y_prev) / sqrt(y_prev_x ** 2 + x_prev_x ** 2) + y = (y_prev_x * x_prev - x_prev_x * y_prev) / sqrt(y_prev_x**2 + x_prev_x**2) return y @@ -34,7 +35,7 @@ def get_P(x, y): return P -@dataclass +@dataclasses.dataclass class CIQuantities: energy1: float gradient1: np.ndarray @@ -50,6 +51,11 @@ class CIQuantities: y: np.ndarray energy: float forces: np.ndarray + hessian: Optional[np.ndarray] = None + + def savez(self, fn): + kwargs = dataclasses.asdict(self) + np.savez(fn, **kwargs) class ConicalIntersection(Calculator): @@ -70,7 +76,9 @@ def __init__(self, calculator1, calculator2, **kwargs): def get_energy(self, atoms, coords, **prepare_kwargs): """Energy of calculator 1.""" - return self.calculator1.get_energy(atoms, coords, **prepare_kwargs) + results = self.calculator1.get_energy(atoms, coords, **prepare_kwargs) + self.calc_counter += 1 + return results def get_ci_quantities(self, atoms, coords, **prepare_kwargs): """Relavent quantities including branching plane and projector P.""" @@ -146,6 +154,8 @@ def get_forces(self, atoms, coords, **prepare_kwargs): """Projected gradient for CI optimization.""" ciq = self.get_ci_quantities(atoms, coords, **prepare_kwargs) + ciq.savez(self.make_fn("ci_quantities.npz")) + self.calc_counter += 1 return { "energy": ciq.energy, "forces": ciq.forces, @@ -164,6 +174,9 @@ def get_hessian(self, atoms, coords, **prepare_kwargs): hessian_mean = (hessian1 + hessian2) / 2 hessian_proj = ciq.P.dot(hessian_mean).dot(ciq.P) + ciq.hessian = hessian_proj + ciq.savez(self.make_fn("ci_quantities.npz")) + self.calc_counter += 1 return { "energy": ciq.energy, "forces": ciq.forces, diff --git a/pysisyphus/calculators/DFTBp.py b/pysisyphus/calculators/DFTBp.py index f083838741..85a3f71525 100644 --- a/pysisyphus/calculators/DFTBp.py +++ b/pysisyphus/calculators/DFTBp.py @@ -35,13 +35,15 @@ def parse_xplusy(text): for i in range(states): block = lines[i * block_size : (i + 1) * block_size] _, *rest = block - xpy = np.array([line.split() for line in rest], dtype=float) + xpy = list() + for line in block[1:]: + xpy.extend(line.split()) + xpy = np.array(xpy, dtype=float) xpys.append(xpy) return size, states, np.array(xpys) class DFTBp(OverlapCalculator): - conf_key = "dftbp" _set_plans = ( "out", @@ -95,7 +97,7 @@ class DFTBp(OverlapCalculator): }, } - def __init__(self, parameter, *args, slakos=None, root=None, **kwargs): + def __init__(self, parameter, *args, slakos=None, **kwargs): super().__init__(*args, **kwargs) assert self.mult == 1, "Open-shell not yet supported!" @@ -107,8 +109,6 @@ def __init__(self, parameter, *args, slakos=None, root=None, **kwargs): f"Expected '{self.parameter}' sub-directory in '{self.slakos_prefix}' " "but could not find it!" ) - self.root = root - self.base_cmd = self.get_cmd() self.gen_geom_fn = "geometry.gen" self.inp_fn = "dftb_in.hsd" @@ -203,8 +203,8 @@ def get_gen_str(atoms, coords): return gen_str @staticmethod - def get_excited_state_str(root, forces=False): - if root is None: + def get_excited_state_str(track, root, nroots, forces=False): + if (nroots is None) and (root is None) and (track == False): return "" casida_tpl = jinja2.Template( @@ -213,7 +213,7 @@ def get_excited_state_str(root, forces=False): Casida { NrOfExcitations = {{ nstates }} Symmetry = Singlet - StateOfInterest = {{ root }} + {% if root %}StateOfInterest = {{ root }}{% endif %} WriteXplusY = Yes {{ es_forces }} } @@ -222,13 +222,14 @@ def get_excited_state_str(root, forces=False): ) es_forces = "ExcitedStateForces = Yes" if forces else "" es_str = casida_tpl.render( - nstates=root + 5, + nstates=nroots if nroots else root + 5, root=root, es_forces=es_forces, ) return es_str def prepare_input(self, atoms, coords, calc_type): + atoms = [atom.capitalize() for atom in atoms] path = self.prepare_path(use_in_run=True) gen_str = self.get_gen_str(atoms, coords) with open(path / self.gen_geom_fn, "w") as handle: @@ -236,7 +237,7 @@ def prepare_input(self, atoms, coords, calc_type): analysis = list() if calc_type == "forces": analysis.append("CalculateForces = Yes") - if self.root: + if self.track or self.root: analysis.extend(("WriteEigenvectors = Yes", "EigenvectorsAsText = Yes")) ang_moms = self.max_ang_moms[self.parameter] unique_atoms = set(atoms) @@ -265,7 +266,9 @@ def prepare_input(self, atoms, coords, calc_type): parameter=self.parameter, max_ang_moms=max_ang_moms, hubbard_derivs=hubbard_derivs, - excited_state_str=self.get_excited_state_str(self.root, es_forces), + excited_state_str=self.get_excited_state_str( + self.track, self.root, self.nroots, es_forces + ), analysis=analysis, ) return inp, path @@ -287,6 +290,9 @@ def get_energy(self, atoms, coords, **prepare_kwargs): ) return results + def get_all_energies(self, atoms, coords, **prepare_kwargs): + return self.get_energy(atoms, coords, **prepare_kwargs) + def get_forces(self, atoms, coords, **prepare_kwargs): inp, path = self.prepare_input(atoms, coords, "forces") run_kwargs = { @@ -316,9 +322,9 @@ def run_calculation(self, atoms, coords, **prepare_kwargs): "hold": self.track, } results = self.run(inp, **run_kwargs) - if self.track: - self.calc_counter += 1 - self.store_overlap_data(atoms, coords, path) + results = self.store_and_track( + results, self.run_calculation, atoms, coords, **prepare_kwargs + ) return results def parse_total_energy(self, text): @@ -394,7 +400,7 @@ def prepare_overlap_data(self, path): # with open(path / "detailed.out") as handle: with open(self.out) as handle: detailed = handle.read() - all_energies = self.parse_all_energies(path) + all_energies = self.parse_all_energies(out_fn=self.out) # # MO coefficients diff --git a/pysisyphus/calculators/Dimer.py b/pysisyphus/calculators/Dimer.py index dfc586d2eb..fc3d3a6270 100644 --- a/pysisyphus/calculators/Dimer.py +++ b/pysisyphus/calculators/Dimer.py @@ -420,7 +420,7 @@ def remove_translation(self, displacement): if max(abs(average)) > 1e-8: self.log( - f"N-vector not translationally invariant. Removing average before normalization." + "N-vector not translationally invariant. Removing average before normalization." ) else: return displacement diff --git a/pysisyphus/calculators/EGO.py b/pysisyphus/calculators/EGO.py index 12a8de0aed..f7df35a66b 100644 --- a/pysisyphus/calculators/EGO.py +++ b/pysisyphus/calculators/EGO.py @@ -1,5 +1,6 @@ # [1] http://dx.doi.org/10.1021/acs.jctc.8b00885 # Exploring Potential Energy Surface with External Forces +# Wolinski, 2018 import numpy as np @@ -26,37 +27,45 @@ def __init__( self._ref_hessian = None self._s = None + def set_ref_hessian_for_geom(self, ref_geom): + # Calculate actual Hessian with internal calculator + results = self.calculator.get_hessian(ref_geom.atoms, ref_geom.cart_coords) + self._ref_hessian = results["hessian"] + Hr0 = self._ref_hessian @ self.ref_geom.cart_coords[:, None] + # As shown in left column on p. 6309 of [1] + self.s = self.max_force / np.abs(Hr0).max() + self.log(f"Set EGO s={self._s:.6f}") + @property def ref_hessian(self): if self._ref_hessian is None: - geom = self.ref_geom - results = self.calculator.get_hessian(geom.atoms, geom.cart_coords) - self._ref_hessian = results["hessian"] - Hr0 = self._ref_hessian @ self.ref_geom.cart_coords[:, None] - self._s = self.max_force / np.abs(Hr0).max() - self.log(f"Set EGO s={self._s:.6f}") + self.set_ref_hessian_for_geom(self.ref_geom) return self._ref_hessian @property def s(self): return self._s - def get_mods(self, atoms, coords): + @s.setter + def s(self, s): + self._s = s + + def get_external_forces(self, atoms, coords): assert atoms == self.ref_geom.atoms Hr = self.ref_hessian @ coords[:, None] - s = self.s energy_mod = float(coords[None, :] @ Hr) - forces_mod = -s * Hr.flatten() + forces_mod = -self.s * Hr.flatten() return energy_mod, forces_mod def get_energy(self, atoms, coords, **prepare_kwargs): true_energy = self.calculator.get_energy(atoms, coords)["energy"] - energy_mod, _ = self.get_mods(atoms, coords) + energy_mod, _ = self.get_external_forces(atoms, coords) results = { "energy": true_energy + energy_mod, "true_energy": true_energy, } + # Manually increase calculation counter of EGO calculator self.calc_counter += 1 return results @@ -64,7 +73,7 @@ def get_forces(self, atoms, coords, **prepare_kwargs): true_results = self.calculator.get_forces(atoms, coords, **prepare_kwargs) true_energy = true_results["energy"] true_forces = true_results["forces"] - energy_mod, forces_mod = self.get_mods(atoms, coords) + energy_mod, forces_mod = self.get_external_forces(atoms, coords) results = { "energy": true_energy + energy_mod, @@ -72,5 +81,9 @@ def get_forces(self, atoms, coords, **prepare_kwargs): "true_forces": true_forces, "true_energy": true_energy, } + # Manually increase calculation counter of EGO calculator self.calc_counter += 1 return results + + def get_hessian(self, atoms, coords, **prepare_kwargs): + raise NotImplementedError("EGO-Hessian is not implemented!") diff --git a/pysisyphus/calculators/EnergyMin.py b/pysisyphus/calculators/EnergyMin.py index 18e063c760..7baea841cf 100644 --- a/pysisyphus/calculators/EnergyMin.py +++ b/pysisyphus/calculators/EnergyMin.py @@ -41,7 +41,6 @@ def __init__( mix Enable mixing of both forces, according to the approach outlined in [2]. Can be used to optimize guesses for MECPs. - Pass alpha Smoothing parameter in Hartree. See [2] for a discussion. sigma @@ -49,15 +48,15 @@ def __init__( smaller for bigga sigmas. Has to be adapted for each case. See [2] for a discussion (p. 407 right column and p. 408 left column.) min_energy_diff - Energy difference in Hartree. When set to a value != 0 and the - energy difference between both + Energy difference in Hartree. + When set to a value != 0 and the energy difference between both calculators drops below this value, execution of both calculations - is diabled for 'check_after' cycles. In these cycles the calculator choice - remains fixed. After 'check_after' cycles, both energies - will be calculated and it is checked, if the previous calculator - choice remains valid. + is diabled for 'check_after' cycles. In these cycles the calculator + choice remains fixed, that is only one state is calculated. After + 'check_after' cycles, both energies will be calculated again and it + is checked, if the previous calculator choice is still valid. In conjunction with 'check_after' both arguments can be used to - save computational ressources. + save computational ressources and to speed up the calculations. check_after Amount of cycles in which the calculator choice remains fixed. @@ -118,6 +117,16 @@ def run_calculation(calc, name=name): energy2 = results2["energy"] all_energies = np.array((energy1, energy2)) + min_ind = [1, 0][int(energy1 < energy2)] + en1_or_en2 = ("calc1", "calc2")[min_ind] + energy_diff = energy1 - energy2 + energy_diff_kJ = abs(energy_diff) * AU2KJPERMOL + + self.log( + f"@ energy_calc1={energy1:.6f} au, energy_calc2={energy2:.6f} au, " + f"|ΔE|={energy_diff_kJ: >10.2f} kJ mol⁻¹." + ) + # Mixed forces to optimize crossing points if self.mix: # Must be positive, so substract lower energy from higher energy. @@ -167,16 +176,12 @@ def run_calculation(calc, name=name): return results # Mixed forces end - min_ind = [1, 0][int(energy1 < energy2)] - en1_or_en2 = ("calc1", "calc2")[min_ind] - energy_diff = energy1 - energy2 # Try to fix calculator, if requested if (self.min_energy_diff and self.check_after) and ( # When the actual difference is above to minimum differences # or # no calculator is fixed yet - (energy_diff > self.min_energy_diff) - or (self.fixed_calc is None) + (energy_diff > self.min_energy_diff) or (self.fixed_calc is None) ): self.fixed_calc = (self.calc1, self.calc2)[min_ind] self.recalc_in = self.check_after @@ -185,11 +190,9 @@ def run_calculation(calc, name=name): ) results = (results1, results2)[min_ind] results["all_energies"] = all_energies - energy_diff_kJ = abs(energy_diff) * AU2KJPERMOL self.log( - f"energy_calc1={energy1:.6f} au, energy_calc2={energy2:.6f} au, returning " - f"results for {en1_or_en2}, {energy_diff_kJ: >10.2f} kJ mol⁻¹ lower." + f"Returning results for {en1_or_en2}, ({energy_diff_kJ: >10.2f} kJ mol⁻¹ lower)." ) self.calc_counter += 1 return results diff --git a/pysisyphus/calculators/FakeASE.py b/pysisyphus/calculators/FakeASE.py index 913b405674..0ef644b818 100644 --- a/pysisyphus/calculators/FakeASE.py +++ b/pysisyphus/calculators/FakeASE.py @@ -1,6 +1,13 @@ +import warnings + from pysisyphus.constants import BOHR2ANG + class FakeASE: + """Pysisyphus calculator mimicing an ASE calculator. + + Instances of this class can be set as calculators on ASE Atoms + objects.""" def __init__(self, calc): self.calc = calc @@ -8,12 +15,15 @@ def __init__(self, calc): self.results = dict() def get_atoms_coords(self, atoms): - return (atoms.get_chemical_symbols(), - # Convert ASE Angstrom to Bohr for pysisyphus - atoms.get_positions().flatten() / BOHR2ANG + return ( + atoms.get_chemical_symbols(), + # Convert ASE Angstrom to Bohr for pysisyphus + atoms.get_positions().flatten() / BOHR2ANG, ) - def get_potential_energy(self, atoms=None): + def get_potential_energy(self, atoms=None, force_consistent=True): + if not force_consistent: + warnings.warn("force_consistent=False is ignored by FakeASE!") atoms, coords = self.get_atoms_coords(atoms) results = self.calc.get_energy(atoms, coords) diff --git a/pysisyphus/calculators/FourWellAnaPot.py b/pysisyphus/calculators/FourWellAnaPot.py index 1c43bc57f1..df4abd46cb 100644 --- a/pysisyphus/calculators/FourWellAnaPot.py +++ b/pysisyphus/calculators/FourWellAnaPot.py @@ -4,16 +4,19 @@ # https://doi.org/10.1063/1.1885467 # Eq. (11) -class FourWellAnaPot(AnaPotBase): - def __init__(self): +class FourWellAnaPot(AnaPotBase): + def __init__(self): V_str = "x**4 + y**4 - 2*x**2 - 4*y**2 + x*y + 0.3*x + 0.1*y" - xlim = (-1.75, 1.75) - ylim = (-1.75, 1.75) + # xlim = (-1.75, 1.75) + # ylim = (-1.75, 1.75) + lim = 2.0 + xlim = (-lim, lim) + ylim = (-lim, lim) minima = ( (1.12410175, -1.48527428, 0.0), (-0.82190767, -1.36672971, 0.0), - (-1.17405609, 1.47708706, 0.0), + (-1.17405609, 1.47708706, 0.0), ) super().__init__(V_str=V_str, xlim=xlim, ylim=ylim, minima=minima) @@ -23,6 +26,4 @@ def __str__(self): if __name__ == "__main__": fw = FourWellAnaPot() - fw.plot() - import matplotlib.pyplot as plt - plt.show() + fw.plot(show=True) diff --git a/pysisyphus/calculators/Gaussian16.py b/pysisyphus/calculators/Gaussian16.py index 93381e7f66..407693f391 100644 --- a/pysisyphus/calculators/Gaussian16.py +++ b/pysisyphus/calculators/Gaussian16.py @@ -5,13 +5,162 @@ import shutil import subprocess import textwrap +import warnings import numpy as np import pyparsing as pp -from pysisyphus.calculators.OverlapCalculator import OverlapCalculator +from pysisyphus.calculators.OverlapCalculator import ( + GroundStateContext, + OverlapCalculator, +) from pysisyphus.constants import AU2EV, BOHR2ANG +from pysisyphus.wavefunction.excited_states import norm_ci_coeffs from pysisyphus.helpers_pure import file_or_str +from pysisyphus.io import fchk as io_fchk + + +NMO_RE = re.compile( + r"NBasis=\s+(?P\d+)\s+NAE=\s+(?P\d+)\s+NBE=\s+(?P\d+)" +) +RESTRICTED_RE = re.compile("RHF ground state") +TRANS_PATTERN = ( + r"(?:\d+(?:A|B){0,1})\s+" + r"(?:\->|\<\-)\s+" + r"(?:\d+(?:A|B){0,1})\s+" + r"(?:[\d\-\.]+)\s+" +) +# Excited State 2: 2.009-?Sym 7.2325 eV 171.43 nm f=0.0000 =0.759 +EXC_STATE_RE = re.compile( + r"Excited State\s+(?P\d+):\s+" + r"(?P