Source code for chemtools.conceptual.general

# -*- coding: utf-8 -*-
# ChemTools is a collection of interpretive chemical tools for
# analyzing outputs of the quantum chemistry calculations.
#
# Copyright (C) 2016-2019 The ChemTools Development Team
#
# This file is part of ChemTools.
#
# ChemTools is free software; you can redistribute it and/or
# modify it under the terms of the GNU General Public License
# as published by the Free Software Foundation; either version 3
# of the License, or (at your option) any later version.
#
# ChemTools is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program; if not, see <http://www.gnu.org/licenses/>
#
# --
"""Conceptual Density Functional Theory (DFT) Reactivity Tools Based on General Energy Model.

This module contains the global and local tool classes corresponding to user-specified energy
models.
"""


import logging
import numpy as np
import sympy as sp

from scipy.optimize import root, least_squares

from chemtools.conceptual.utils import check_number_electrons
from chemtools.utils.utils import doc_inherit
from chemtools.conceptual.base import BaseGlobalTool


__all__ = ["GeneralGlobalTool"]


[docs]class GeneralGlobalTool(BaseGlobalTool): r""" Class of global conceptual DFT reactivity descriptors based on the user-specified energy model. The energy model is approximated as a symbolic function of the number of electrons, and the unknown parameters are obtained by interpolating to the given values of energy; i.e. the number of parameters in the model should equal the number of given energy values and the corresponding number of electrons. The :math:`n^{\text{th}}`-order derivative of the symbolic energy model with respect to the number of electrons at fixed external potential is calculated symbolically. """ def __init__(self, expr, n0, n_energies, n_symbol=None, n0_symbol=None, guess=None, opts=None): """ Initialize class. Parameters ---------- expr : sp.Exp The energy expression representing the dependence of energy on the number of electrons. n0 : float Reference number of electrons, i.e. :math:`N_0`. n_energies : dict The energy values of `expr` at different electron-numbers. The dict has int (electron-number) keys, float (energy) values. n_symbol : sp.Symbol, default=sp.symbols('N') The symbol in `expr` that represents the number of electrons. n0_symbol: sp.Symbol, optional The symbol in `expr` that represents the electron-number at which to evaluate `expr`. If not specified, assume that it is already expressed numerically in `expr`. guess : dict, optional Guesses at the values of the parameters of `expr`. The dict has sp.Symbol keys, float values. opts : dict, optional Optional keyword arguments to pass to the :py:meth:`scipy.optimize.root` solver that is used to solve for the parameters in the model. """ # make sure that the energy expression depends on number of electrons if n_symbol is None: n_symbol = sp.symbols('N') if n_symbol not in expr.atoms(sp.Symbol): raise ValueError( 'The expr={0} does not contain {1} symbol representing '.format(expr, n_symbol) + 'the number of electrons.') self._n_symb = n_symbol # store minimum and maximum number of electrons used for interpolation self._n_min, self._n_max = np.min(n_energies.keys()), np.max(n_energies.keys()) # substitute N0 in energy expression if n0_symbol: expr = expr.subs(n0_symbol, n0) # list of energy model parameters params = expr.atoms(sp.Symbol) params.remove(self._n_symb) # assign initial values for parameters of energy model if guess is None: guess = {} guess.update({param: 1. for param in params if param not in guess}) # solve for the parameters of energy model self._params = self._solve_parameters(expr, n_energies, guess, opts) # substitute values of parameters in energy expression self._expr = expr.subs(self._params.items()) # solve for N_max (number of electrons for which the 1st derivative of energy is zero) n_max = self._solve_nmax(n0) super(GeneralGlobalTool, self).__init__(n0, n_max) @property def params(self): """Parameter dictionary of energy model.""" return self._params @property def n_symbol(self): """Symbol used to denote the number of electrons.""" return self._n_symb @property def expression(self): """Energy expression as a function of number of electrons, :math:`E(N)`.""" return self._expr
[docs] @doc_inherit(BaseGlobalTool) def energy(self, n_elec): # check n_elec argument check_number_electrons(n_elec, self._n_min, self._n_max) # evaluate energy value = self._expr.subs(self._n_symb, n_elec) return value
[docs] @doc_inherit(BaseGlobalTool) def energy_derivative(self, n_elec, order=1): # check n_elec argument check_number_electrons(n_elec, self._n_min, self._n_max) # check order if not (isinstance(order, int) and order > 0): raise ValueError("Argument order should be an integer greater than or equal to 1.") # obtain derivative expression deriv = self._expr.diff(self._n_symb, order) # evaluate derivative expression at n_elec deriv = deriv.subs(self._n_symb, n_elec) return deriv
def _solve_parameters(self, expr, n_energies, guess, opts=None): r""" Solve for the unknown parameters of the energy model. The parameters of energy model is found using the given :math:`\left(N, E(N)\right)` pairs. This requires solving a system of (non-)linear equations Parameters ---------- See __init__(). Returns ------- parameters : dict A dictionary of sympy.Symbol keys corresponding to the value of the expression's solved parameters. """ # obtain set of parameters in the energy expression params = guess.keys() if len(params) == 0: raise ValueError( 'There is no parameters in the energy_expression={0} to solve for.'.format(expr)) if not all([param in expr.atoms(sp.Symbol) for param in params]): raise ValueError('The expr={0} does not contain parameters given in guess={1}' ''.format(expr, guess)) if len(params) > len(n_energies): raise ValueError('Underdetermined system of equations: Number of unknowns parameters ' 'in the energy model is more than number of given known energies.') # initial guess for the parameters in the energy model guess = np.array([guess[param] for param in params]) # construct system of equations to solve system_eqns = [] d_system_eqns = [] for n, energy in n_energies.iteritems(): eqn = sp.lambdify((params,), expr.subs(self._n_symb, n) - energy, 'numpy') system_eqns.append(eqn) d_eqn_row = [] for p in params: d_eqn = sp.lambdify((params,), expr.diff(p).subs(self._n_symb, n), 'numpy') d_eqn_row.append(d_eqn) d_system_eqns.append(d_eqn_row) def objective(args): """ Evaluate the system of equations for the given values of parameters. Parameters ---------- args : array representing the value of parameters. The expression for the property. """ return np.array([eqn(args) for eqn in system_eqns]) def jacobian(args): """ Evaluate jacobian of the objective function for the given values of parameters. Parameters ---------- See objective(). """ jac = [] for row in d_system_eqns: jac.append([eqn(args) for eqn in row]) return np.array(jac) # solve for the parameters in the energy model if opts is None: opts = {} result = least_squares(objective, guess, jac=jacobian, **opts) if not result.success: raise ValueError( 'The system of equations for parameters could not be solved. ' 'message:{0}'.format(result.message)) # make dictionary of parameter values parameters = dict([(param, result.x[i]) for i, param in enumerate(params)]) return parameters def _solve_nmax(self, guess): r"""Solve for the :math:`N_{\text{max}}` of the energy model.""" d_expr = self._expr.diff(self._n_symb) n_max_eqn = sp.lambdify(self._n_symb, d_expr, 'numpy') result = root(n_max_eqn, guess) print result if result.success: n_max = np.asscalar(result.x) # n_ceil = math.ceil(n_max) # n_floor = math.floor(n_max) # e_ceil = self._expr.subs(self._n_symb, math.ceil(n_max)) # e_floor = self._expr.subs(self._n_symb, math.floor(n_max)) # n_max = n_floor if e_floor < e_ceil else n_ceil else: for sign in (+1, -1): n_inf = n_max_eqn(sign * np.inf) if np.isfinite(n_inf): n_max = sign * np.inf break else: n_max = None logging.warning("The system of equations for Nmax could not be solved; " "Nmax=`None`. message:{0}".format(result.message)) return n_max