CuestPCMIntPlan

    'RN' : 2.20,
    'FR' : 3.48,
    'RA' : 2.83,
    'AC' : 2.47,
    'TH' : 2.45,
    'PA' : 2.43,
    'U'  : 2.41,
    'NP' : 2.39,
    'PU' : 2.43,
    'AM' : 2.44,
    'CM' : 2.45,
    'BK' : 2.44,
    'CF' : 2.45,
    'ES' : 2.45,
    'FM' : 2.45,
    'MD' : 2.46,
    'NO' : 2.46,
    'LR' : 2.46,
    }

class CuestPCMIntPlan(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        intPlan : CuestOEIntPlan,
        epsilon : float,
        x_prefactor : float,
        num_angular_points_per_hydrogen_atom : int,
        num_angular_points_per_heavy_atom : int,
        Ns : np.ndarray,
        effective_nuclear_charges : np.ndarray,
        cutoff : float,
        convergence_tol : float,
        atomic_radii_scale=1.2
        ):

        bohr_per_ang=UnitConversions.conversions['bohr_per_ang']

        if num_angular_points_per_hydrogen_atom not in _zeta_map:
            raise RuntimeError(f"Invalid num_angular_points_per_hydrogen_atom value ({num_angular_points_per_hydrogen_atom}) is not a valid Lebedev number")
        if num_angular_points_per_heavy_atom not in _zeta_map:
            raise RuntimeError(f"Invalid num_angular_points_per_heavy_atom value ({num_angular_points_per_heavy_atom}) is not a valid Lebedev number")

        self.initialized = False

        self.handle = handle
        self.num_angular_points_per_hydrogen_atom=num_angular_points_per_hydrogen_atom
        self.num_angular_points_per_heavy_atom=num_angular_points_per_heavy_atom
        self.epsilon=epsilon
        self.x_prefactor=x_prefactor
        self.cutoff = cutoff
        self.convergence_tol = convergence_tol

        symbols_upper = [PeriodicTable.N_to_symbol_upper_table[N] for N in Ns]
        for sym in symbols_upper:
            if sym not in _bondi_radii_ang:
                raise RuntimeError(f"No Bondi radius defined for element {sym}")
        grid_sizes = [num_angular_points_per_hydrogen_atom if s == "H" else num_angular_points_per_heavy_atom for s in symbols_upper]
        zetas=[_zeta_map[grid_size] for grid_size in grid_sizes]
        atomic_radii=atomic_radii_scale*np.array([_bondi_radii_ang[symbol_upper] * bohr_per_ang for symbol_upper in symbols_upper])

        pcm_int_plan_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_PCMINTPLAN_PARAMETERS,
            )

        pcm_cutoff = ce.data_double(self.cutoff)
        status = ce.cuestParametersConfigure(
            parametersType=ce.CuestParametersType.CUEST_PCMINTPLAN_PARAMETERS,
            parameters=pcm_int_plan_parameters.parameters,
            attribute=ce.CuestPCMIntPlanParametersAttributes.CUEST_PCMINTPLAN_PARAMETERS_CUTOFF,
            attributeValue=pcm_cutoff,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestParametersConfigure failed')

        pcm_x_prefactor = ce.data_double(self.x_prefactor)
        status = ce.cuestParametersConfigure(
            parametersType=ce.CuestParametersType.CUEST_PCMINTPLAN_PARAMETERS,
            parameters=pcm_int_plan_parameters.parameters,
            attribute=ce.CuestPCMIntPlanParametersAttributes.CUEST_PCMINTPLAN_PARAMETERS_X_PREFACTOR,
            attributeValue=pcm_x_prefactor,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestParametersConfigure failed')

        self.pcm_int_plan_handle = ce.cuestPCMIntPlanHandle()

        # => Workspace Query <= #

        persistent_workspace_descriptor = CuestWorkspaceDescriptor()
        temporary_workspace_descriptor = CuestWorkspaceDescriptor()

        status = ce.cuestPCMIntPlanCreateWorkspaceQuery(
            handle=handle.handle,
            intPlan=intPlan.oe_int_plan_handle,
            parameters=pcm_int_plan_parameters.parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            numAngularPointsPerAtom=grid_sizes,
            epsilon=epsilon,
            zetas=zetas,
            atomicRadii=atomic_radii,
            effectiveNuclearCharges=effective_nuclear_charges,
            outPlan=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanCreateWorkspaceQuery failed')

        persistent_workspace = CuestWorkspace(workspaceDescriptor=persistent_workspace_descriptor)
        temporary_workspace = CuestWorkspace(workspaceDescriptor=temporary_workspace_descriptor)

        # => Creation <= #

        status = ce.cuestPCMIntPlanCreate(
            handle=handle.handle,
            intPlan=intPlan.oe_int_plan_handle,
            parameters=pcm_int_plan_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            numAngularPointsPerAtom=grid_sizes,
            epsilon=epsilon,
            zetas=zetas,
            atomicRadii=atomic_radii,
            effectiveNuclearCharges=effective_nuclear_charges,
            outPlan=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanCreate failed')

        # Bind the lifetime of persistent_workspace to this object
        self.persistent_workspace = persistent_workspace

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestPCMIntPlanDestroy(
            handle=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanDestroy failed')

    @memoized_property
    def npoint(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_POINT,
            attributeValue=npoint,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    @memoized_property
    def n_active_point(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_ACTIVE_POINT,
            attributeValue=npoint,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    # => String Details <= #

    def __str__(self):
        return self.string

    @property
    def string(self):
        s = ''
        s += 'PCMIntPlan:\n'
        s += '%-10s = %10.4f\n' % ('dielectric', self.epsilon)
        s += '%-10s = %10.1e\n' % ('tolerance', self.convergence_tol)
        s += '%-10s = %10.1e\n' % ('cutoff', self.cutoff)
        s += '%-10s = %10d\n'   % ('npoint', self.npoint)
        s += '%-10s = %10d\n'   % ('nactive', self.n_active_point)
        return s


    'AC' : 2.47,
    'TH' : 2.45,
    'PA' : 2.43,
    'U'  : 2.41,
    'NP' : 2.39,
    'PU' : 2.43,
    'AM' : 2.44,
    'CM' : 2.45,
    'BK' : 2.44,
    'CF' : 2.45,
    'ES' : 2.45,
    'FM' : 2.45,
    'MD' : 2.46,
    'NO' : 2.46,
    'LR' : 2.46,
    }

class CuestPCMIntPlan(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        intPlan : CuestOEIntPlan,
        epsilon : float,
        x_prefactor : float,
        num_angular_points_per_hydrogen_atom : int,
        num_angular_points_per_heavy_atom : int,
        Ns : np.ndarray,
        effective_nuclear_charges : np.ndarray,
        cutoff : float,
        convergence_tol : float,
        atomic_radii_scale=1.2
        ):

        bohr_per_ang=UnitConversions.conversions['bohr_per_ang']

        if num_angular_points_per_hydrogen_atom not in _zeta_map:
            raise RuntimeError(f"Invalid num_angular_points_per_hydrogen_atom value ({num_angular_points_per_hydrogen_atom}) is not a valid Lebedev number")
        if num_angular_points_per_heavy_atom not in _zeta_map:
            raise RuntimeError(f"Invalid num_angular_points_per_heavy_atom value ({num_angular_points_per_heavy_atom}) is not a valid Lebedev number")

        self.initialized = False

        self.handle = handle
        self.num_angular_points_per_hydrogen_atom=num_angular_points_per_hydrogen_atom
        self.num_angular_points_per_heavy_atom=num_angular_points_per_heavy_atom
        self.epsilon=epsilon
        self.x_prefactor=x_prefactor
        self.cutoff = cutoff
        self.convergence_tol = convergence_tol

        symbols_upper = [PeriodicTable.N_to_symbol_upper_table[N] for N in Ns]
        for sym in symbols_upper:
            if sym not in _bondi_radii_ang:
                raise RuntimeError(f"No Bondi radius defined for element {sym}")
        grid_sizes = [num_angular_points_per_hydrogen_atom if s == "H" else num_angular_points_per_heavy_atom for s in symbols_upper]
        zetas=[_zeta_map[grid_size] for grid_size in grid_sizes]
        atomic_radii=atomic_radii_scale*np.array([_bondi_radii_ang[symbol_upper] * bohr_per_ang for symbol_upper in symbols_upper])

        pcm_int_plan_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_PCMINTPLAN_PARAMETERS,
            )

        pcm_cutoff = ce.data_double(self.cutoff)
        status = ce.cuestParametersConfigure(
            parametersType=ce.CuestParametersType.CUEST_PCMINTPLAN_PARAMETERS,
            parameters=pcm_int_plan_parameters.parameters,
            attribute=ce.CuestPCMIntPlanParametersAttributes.CUEST_PCMINTPLAN_PARAMETERS_CUTOFF,
            attributeValue=pcm_cutoff,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestParametersConfigure failed')

        pcm_x_prefactor = ce.data_double(self.x_prefactor)
        status = ce.cuestParametersConfigure(
            parametersType=ce.CuestParametersType.CUEST_PCMINTPLAN_PARAMETERS,
            parameters=pcm_int_plan_parameters.parameters,
            attribute=ce.CuestPCMIntPlanParametersAttributes.CUEST_PCMINTPLAN_PARAMETERS_X_PREFACTOR,
            attributeValue=pcm_x_prefactor,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestParametersConfigure failed')

        self.pcm_int_plan_handle = ce.cuestPCMIntPlanHandle()

        # => Workspace Query <= #

        persistent_workspace_descriptor = CuestWorkspaceDescriptor()
        temporary_workspace_descriptor = CuestWorkspaceDescriptor()

        status = ce.cuestPCMIntPlanCreateWorkspaceQuery(
            handle=handle.handle,
            intPlan=intPlan.oe_int_plan_handle,
            parameters=pcm_int_plan_parameters.parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            numAngularPointsPerAtom=grid_sizes,
            epsilon=epsilon,
            zetas=zetas,
            atomicRadii=atomic_radii,
            effectiveNuclearCharges=effective_nuclear_charges,
            outPlan=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanCreateWorkspaceQuery failed')

        persistent_workspace = CuestWorkspace(workspaceDescriptor=persistent_workspace_descriptor)
        temporary_workspace = CuestWorkspace(workspaceDescriptor=temporary_workspace_descriptor)

        # => Creation <= #

        status = ce.cuestPCMIntPlanCreate(
            handle=handle.handle,
            intPlan=intPlan.oe_int_plan_handle,
            parameters=pcm_int_plan_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            numAngularPointsPerAtom=grid_sizes,
            epsilon=epsilon,
            zetas=zetas,
            atomicRadii=atomic_radii,
            effectiveNuclearCharges=effective_nuclear_charges,
            outPlan=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanCreate failed')

        # Bind the lifetime of persistent_workspace to this object
        self.persistent_workspace = persistent_workspace

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestPCMIntPlanDestroy(
            handle=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanDestroy failed')

    @memoized_property
    def npoint(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_POINT,
            attributeValue=npoint,


            handle=handle.handle,
            intPlan=intPlan.oe_int_plan_handle,
            parameters=pcm_int_plan_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            numAngularPointsPerAtom=grid_sizes,
            epsilon=epsilon,
            zetas=zetas,
            atomicRadii=atomic_radii,
            effectiveNuclearCharges=effective_nuclear_charges,
            outPlan=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanCreate failed')

        # Bind the lifetime of persistent_workspace to this object
        self.persistent_workspace = persistent_workspace

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestPCMIntPlanDestroy(
            handle=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanDestroy failed')

    @memoized_property
    def npoint(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_POINT,
            attributeValue=npoint,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    @memoized_property
    def n_active_point(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,


npoint

            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanCreate failed')

        # Bind the lifetime of persistent_workspace to this object
        self.persistent_workspace = persistent_workspace

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestPCMIntPlanDestroy(
            handle=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanDestroy failed')

    @memoized_property
    def npoint(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_POINT,
            attributeValue=npoint,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    @memoized_property
    def n_active_point(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_ACTIVE_POINT,
            attributeValue=npoint,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    # => String Details <= #

    def __str__(self):
        return self.string


            handle=self.pcm_int_plan_handle,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestPCMIntPlanDestroy failed')

    @memoized_property
    def npoint(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_POINT,
            attributeValue=npoint,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    @memoized_property
    def n_active_point(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_ACTIVE_POINT,
            attributeValue=npoint,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    # => String Details <= #

    def __str__(self):
        return self.string

    @property
    def string(self):
        s = ''
        s += 'PCMIntPlan:\n'
        s += '%-10s = %10.4f\n' % ('dielectric', self.epsilon)
        s += '%-10s = %10.1e\n' % ('tolerance', self.convergence_tol)
        s += '%-10s = %10.1e\n' % ('cutoff', self.cutoff)
        s += '%-10s = %10d\n'   % ('npoint', self.npoint)
        s += '%-10s = %10d\n'   % ('nactive', self.n_active_point)
        return s


        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    @memoized_property
    def n_active_point(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_ACTIVE_POINT,
            attributeValue=npoint,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    # => String Details <= #

    def __str__(self):
        return self.string

    @property
    def string(self):
        s = ''
        s += 'PCMIntPlan:\n'
        s += '%-10s = %10.4f\n' % ('dielectric', self.epsilon)
        s += '%-10s = %10.1e\n' % ('tolerance', self.convergence_tol)
        s += '%-10s = %10.1e\n' % ('cutoff', self.cutoff)
        s += '%-10s = %10d\n'   % ('npoint', self.npoint)
        s += '%-10s = %10d\n'   % ('nactive', self.n_active_point)
        return s


string

    @memoized_property
    def n_active_point(self):
        npoint = ce.data_uint64_t()
        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_PCMINTPLAN,
            object=self.pcm_int_plan_handle,
            attribute=ce.CuestPCMIntPlanAttributes.CUEST_PCMINTPLAN_NUM_ACTIVE_POINT,
            attributeValue=npoint,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestQuery failed')
        return npoint.value

    # => String Details <= #

    def __str__(self):
        return self.string

    @property
    def string(self):
        s = ''
        s += 'PCMIntPlan:\n'
        s += '%-10s = %10.4f\n' % ('dielectric', self.epsilon)
        s += '%-10s = %10.1e\n' % ('tolerance', self.convergence_tol)
        s += '%-10s = %10.1e\n' % ('cutoff', self.cutoff)
        s += '%-10s = %10d\n'   % ('npoint', self.npoint)
        s += '%-10s = %10d\n'   % ('nactive', self.n_active_point)
        return s


# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cuest.bindings as ce

from .cuest_workspace_descriptor import CuestWorkspaceDescriptor
from .cuest_workspace import CuestWorkspace

from .cuest_handle import CuestHandle
from .cuest_ao_basis import CuestAOBasis

from .cuest_parameters import CuestParameters

import numpy as np

class CuestAOPairList(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        basis : CuestAOBasis,
        xyz : np.ndarray,
        threshold_pq : float,
        ):

        self.initialized = False

        if xyz.ndim != 2: raise RuntimeError('xyz.shape is not (ntom, 3)')
        if xyz.shape != (xyz.shape[0], 3): raise RuntimeError('xyz.shape is not (ntom, 3)')

        self.handle = handle

        ao_pair_list_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            )

        self.ao_pair_list_handle = ce.cuestAOPairListHandle()

        # => Workspace Query <= #

        persistent_workspace_descriptor = CuestWorkspaceDescriptor()
        temporary_workspace_descriptor = CuestWorkspaceDescriptor()

        status = ce.cuestAOPairListCreateWorkspaceQuery(
            handle=handle.handle,
            basis=basis.ao_basis_handle,
            numAtoms=xyz.shape[0],
            xyz=list(xyz.ravel()),
            thresholdPQ=threshold_pq,
            parameters=ao_pair_list_parameters.parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPairList=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListCreateWorkspaceQuery failed')

        persistent_workspace = CuestWorkspace(workspaceDescriptor=persistent_workspace_descriptor)
        temporary_workspace = CuestWorkspace(workspaceDescriptor=temporary_workspace_descriptor)

        # => Creation <= #

        status = ce.cuestAOPairListCreate(
            handle=handle.handle,
            basis=basis.ao_basis_handle,
            numAtoms=xyz.shape[0],
            xyz=list(xyz.ravel()),
            thresholdPQ=threshold_pq,
            parameters=ao_pair_list_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outPairList=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListCreate failed')

        # Bind the lifetime of persistent_workspace to this object
        self.persistent_workspace = persistent_workspace

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestAOPairListDestroy(
            handle=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListDestroy failed')


CuestAOPairList

# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cuest.bindings as ce

from .cuest_workspace_descriptor import CuestWorkspaceDescriptor
from .cuest_workspace import CuestWorkspace

from .cuest_handle import CuestHandle
from .cuest_ao_basis import CuestAOBasis

from .cuest_parameters import CuestParameters

import numpy as np

class CuestAOPairList(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        basis : CuestAOBasis,
        xyz : np.ndarray,
        threshold_pq : float,
        ):

        self.initialized = False

        if xyz.ndim != 2: raise RuntimeError('xyz.shape is not (ntom, 3)')
        if xyz.shape != (xyz.shape[0], 3): raise RuntimeError('xyz.shape is not (ntom, 3)')

        self.handle = handle

        ao_pair_list_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            )

        self.ao_pair_list_handle = ce.cuestAOPairListHandle()

        # => Workspace Query <= #

        persistent_workspace_descriptor = CuestWorkspaceDescriptor()
        temporary_workspace_descriptor = CuestWorkspaceDescriptor()

        status = ce.cuestAOPairListCreateWorkspaceQuery(
            handle=handle.handle,
            basis=basis.ao_basis_handle,
            numAtoms=xyz.shape[0],
            xyz=list(xyz.ravel()),
            thresholdPQ=threshold_pq,
            parameters=ao_pair_list_parameters.parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPairList=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListCreateWorkspaceQuery failed')

        persistent_workspace = CuestWorkspace(workspaceDescriptor=persistent_workspace_descriptor)
        temporary_workspace = CuestWorkspace(workspaceDescriptor=temporary_workspace_descriptor)

        # => Creation <= #

        status = ce.cuestAOPairListCreate(
            handle=handle.handle,
            basis=basis.ao_basis_handle,
            numAtoms=xyz.shape[0],
            xyz=list(xyz.ravel()),
            thresholdPQ=threshold_pq,
            parameters=ao_pair_list_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outPairList=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListCreate failed')

        # Bind the lifetime of persistent_workspace to this object
        self.persistent_workspace = persistent_workspace

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestAOPairListDestroy(
            handle=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListDestroy failed')


# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cuest.bindings as ce

from .cuest_workspace_descriptor import CuestWorkspaceDescriptor
from .cuest_workspace import CuestWorkspace

from .cuest_handle import CuestHandle
from .cuest_ao_basis import CuestAOBasis

from .cuest_parameters import CuestParameters

import numpy as np

class CuestAOPairList(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        basis : CuestAOBasis,
        xyz : np.ndarray,
        threshold_pq : float,
        ):

        self.initialized = False

        if xyz.ndim != 2: raise RuntimeError('xyz.shape is not (ntom, 3)')
        if xyz.shape != (xyz.shape[0], 3): raise RuntimeError('xyz.shape is not (ntom, 3)')

        self.handle = handle

        ao_pair_list_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            )

        self.ao_pair_list_handle = ce.cuestAOPairListHandle()

        # => Workspace Query <= #

        persistent_workspace_descriptor = CuestWorkspaceDescriptor()
        temporary_workspace_descriptor = CuestWorkspaceDescriptor()

        status = ce.cuestAOPairListCreateWorkspaceQuery(
            handle=handle.handle,
            basis=basis.ao_basis_handle,
            numAtoms=xyz.shape[0],
            xyz=list(xyz.ravel()),
            thresholdPQ=threshold_pq,
            parameters=ao_pair_list_parameters.parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPairList=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListCreateWorkspaceQuery failed')

        persistent_workspace = CuestWorkspace(workspaceDescriptor=persistent_workspace_descriptor)
        temporary_workspace = CuestWorkspace(workspaceDescriptor=temporary_workspace_descriptor)

        # => Creation <= #

        status = ce.cuestAOPairListCreate(
            handle=handle.handle,
            basis=basis.ao_basis_handle,
            numAtoms=xyz.shape[0],
            xyz=list(xyz.ravel()),
            thresholdPQ=threshold_pq,
            parameters=ao_pair_list_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outPairList=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListCreate failed')

        # Bind the lifetime of persistent_workspace to this object
        self.persistent_workspace = persistent_workspace

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestAOPairListDestroy(
            handle=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListDestroy failed')


        status = ce.cuestAOPairListCreate(
            handle=handle.handle,
            basis=basis.ao_basis_handle,
            numAtoms=xyz.shape[0],
            xyz=list(xyz.ravel()),
            thresholdPQ=threshold_pq,
            parameters=ao_pair_list_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outPairList=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListCreate failed')

        # Bind the lifetime of persistent_workspace to this object
        self.persistent_workspace = persistent_workspace

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestAOPairListDestroy(
            handle=self.ao_pair_list_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOPairListDestroy failed')


# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cuest.bindings as ce

class CuestParameters(object):

    def __init__(
        self,
        *,
        parametersType,
        ):

        self.initialized = False

        self.parametersType = parametersType
        self.parameters = ce.Parameters()

        status = ce.cuestParametersCreate(
            parametersType=self.parametersType,
            outParameters=self.parameters,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_parameters_create failed')

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestParametersDestroy(
            parametersType=self.parametersType,
            parameters=self.parameters,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_parameters_destroy failed')


        self,
        *,
        parametersType,
        ):

        self.initialized = False

        self.parametersType = parametersType
        self.parameters = ce.Parameters()

        status = ce.cuestParametersCreate(
            parametersType=self.parametersType,
            outParameters=self.parameters,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_parameters_create failed')

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestParametersDestroy(
            parametersType=self.parametersType,
            parameters=self.parameters,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_parameters_destroy failed')


# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cuest.bindings as ce

from .cuest_parameters import CuestParameters

class CuestHandle(object):

    def __init__(
        self,
        ):

        self.initialized = False

        cuest_handle_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            )

        # NOTE: The CuestHandle has a number of defaultable parameters that
        # production codes may wish to expose to the user. Notable among these
        # is the CUDA stream. The CuestHandle will default to the host stream
        # (0), which is blocking. However, production codes may wish to provide
        # a custom stream to the CuestHandle, which will allow for asynchronous
        # cuEST library calls. Any such calls must use CUDA device memory that
        # is synchronized with the stream.

        self.handle = ce.cuestHandle()

        status = ce.cuestCreate(
            parameters=cuest_handle_parameters.parameters,
            handle=self.handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_create failed')

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestDestroy(
            handle=self.handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_destroy failed')

    def set_math_mode(
        self,
        *,
        mode,
        ):

        if not self.initialized: return

        status = ce.cuestSetMathMode(
            handle=self.handle,
            mode=mode,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_set_math_mode failed')


# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import cuest.bindings as ce

from .cuest_parameters import CuestParameters

class CuestHandle(object):

    def __init__(
        self,
        ):

        self.initialized = False

        cuest_handle_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            )

        # NOTE: The CuestHandle has a number of defaultable parameters that
        # production codes may wish to expose to the user. Notable among these
        # is the CUDA stream. The CuestHandle will default to the host stream
        # (0), which is blocking. However, production codes may wish to provide
        # a custom stream to the CuestHandle, which will allow for asynchronous
        # cuEST library calls. Any such calls must use CUDA device memory that
        # is synchronized with the stream.

        self.handle = ce.cuestHandle()

        status = ce.cuestCreate(
            parameters=cuest_handle_parameters.parameters,
            handle=self.handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_create failed')

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestDestroy(
            handle=self.handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_destroy failed')

    def set_math_mode(
        self,
        *,
        mode,
        ):

        if not self.initialized: return


        # NOTE: The CuestHandle has a number of defaultable parameters that
        # production codes may wish to expose to the user. Notable among these
        # is the CUDA stream. The CuestHandle will default to the host stream
        # (0), which is blocking. However, production codes may wish to provide
        # a custom stream to the CuestHandle, which will allow for asynchronous
        # cuEST library calls. Any such calls must use CUDA device memory that
        # is synchronized with the stream.

        self.handle = ce.cuestHandle()

        status = ce.cuestCreate(
            parameters=cuest_handle_parameters.parameters,
            handle=self.handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_create failed')

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestDestroy(
            handle=self.handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_destroy failed')

    def set_math_mode(
        self,
        *,
        mode,
        ):

        if not self.initialized: return

        status = ce.cuestSetMathMode(
            handle=self.handle,
            mode=mode,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_set_math_mode failed')


            parameters=cuest_handle_parameters.parameters,
            handle=self.handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_create failed')

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestDestroy(
            handle=self.handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_destroy failed')

    def set_math_mode(
        self,
        *,
        mode,
        ):

        if not self.initialized: return

        status = ce.cuestSetMathMode(
            handle=self.handle,
            mode=mode,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuest_set_math_mode failed')


# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

from functools import wraps

def memoized_property(fget):
    """
    Return a property attribute for new-style classes that only calls its getter on the first
    access. The result is stored and on subsequent accesses is returned, preventing the need to
    call the getter any more.

    Example::

        >>> class C(object):
        ...     load_name_count = 0
        ...     @memoized_property
        ...     def name(self):
        ...         "name's docstring"
        ...         self.load_name_count += 1
        ...         return "the name"
        >>> c = C()
        >>> c.load_name_count
        0
        >>> c.name
        "the name"
        >>> c.load_name_count
        1
        >>> c.name
        "the name"
        >>> c.load_name_count
        1

    """
    attr_name = '_{0}'.format(fget.__name__)

    @wraps(fget)
    def fget_memoized(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fget(self))
        return getattr(self, attr_name)

    return property(fget_memoized)


        ...     def name(self):
        ...         "name's docstring"
        ...         self.load_name_count += 1
        ...         return "the name"
        >>> c = C()
        >>> c.load_name_count
        0
        >>> c.name
        "the name"
        >>> c.load_name_count
        1
        >>> c.name
        "the name"
        >>> c.load_name_count
        1

    """
    attr_name = '_{0}'.format(fget.__name__)

    @wraps(fget)
    def fget_memoized(self):
        if not hasattr(self, attr_name):
            setattr(self, attr_name, fget(self))
        return getattr(self, attr_name)

    return property(fget_memoized)


# SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

import numpy as np

from .memoized_property import memoized_property

from .molecule import Molecule
from .ao_basis import AOBasis
from .sad_guess import SADGuess
from .diis import DIIS
from .xc_functionals import XCFunctionalInfo

from .cuda_utility import CudaUtility

from .cuest_handle import CuestHandle
from .cuest_ao_basis import CuestAOBasis
from .cuest_molecular_grid import CuestMolecularGrid
from .cuest_ao_pair_list import CuestAOPairList
from .cuest_oe_int_plan import CuestOEIntPlan
from .cuest_oe_int_compute import CuestOEIntCompute
from .cuest_df_int_plan import CuestDFIntPlan
from .cuest_df_int_compute import CuestDFIntCompute
from .cuest_xc_int_plan import CuestXCIntPlan
from .cuest_xc_int_compute import CuestXCIntCompute
from .cuest_pcm_int_plan import CuestPCMIntPlan
from .cuest_pcm_int_compute import CuestPCMIntCompute

from .gpu_matrix import GPUMatrix
from .gpu_matrix_utility import GPUMatrixUtility

import time

class RHF(object):

    def __init__(
        self,
        *,
        cuest_handle : CuestHandle,
        molecule : Molecule,
        charge : int,
        xc_functional_name : str = None,
        primary : AOBasis,
        auxiliary : AOBasis,
        minao : AOBasis,
        primary_name : str = None,
        auxiliary_name : str = None,
        minao_name : str = None,
        print_level : int = 2,
        threshold_pq : float = 1.0E-12,
        threshold_canonical : float = 1.0E-6,
        df_fitting_eigenvalue_cutoff : float = 1.0e-12,
        diis_max_nvector : int = 6,
        maxiter : int = 100,
        g_convergence : float = 1.0E-6,
        xc_threshold_collocation : float = 1.0e-18,
        xc_grid_level : int = 2,
        xc_grid_family : str = "GRID",
        nlc_threshold_collocation : float = 1.0e-18,
        nlc_grid_level : int = 1,
        nlc_grid_family : str = "GRID",
        dfk_int8_slice_count_start : int = 6,
        dfk_int8_modulus_count_start : int = 5,
        dfk_int8_slice_count_end : int = 6,
        dfk_int8_modulus_count_end : int = 9,
        pcm_epsilon: float = 1.0, # 1.0 for gas phase, >1.0 for solvated calc.
        pcm_x_prefactor: float = 0.0, # 0.0 for CPCM, 0.5 for COSMO
        pcm_cutoff: float = 1.0e-10,
        pcm_convergence_tol: float = 1e-10,
        pcm_num_angular_points_per_hydrogen_atom: int = 110,
        pcm_num_angular_points_per_heavy_atom: int = 194,
        ):

        self.cuest_handle = cuest_handle

        self.molecule = molecule
        self.charge = charge

        self.xc_functional = XCFunctionalInfo.string_to_enum(xc_functional_name)

        self.primary = primary
        self.auxiliary = auxiliary
        self.minao = minao

        self.primary_name = primary_name
        self.auxiliary_name = auxiliary_name
        self.minao_name = minao_name

        self.xc_threshold_collocation = xc_threshold_collocation
        self.xc_grid_level = xc_grid_level
        self.xc_grid_family = xc_grid_family

        self.nlc_threshold_collocation = nlc_threshold_collocation
        self.nlc_grid_level = nlc_grid_level
        self.nlc_grid_family = nlc_grid_family

        self.print_level = print_level

        self.threshold_pq = threshold_pq
        self.threshold_canonical = threshold_canonical
        self.df_fitting_eigenvalue_cutoff = df_fitting_eigenvalue_cutoff
        self.diis_max_nvector = diis_max_nvector
        self.maxiter = maxiter
        self.g_convergence = g_convergence

        self.pcm_num_angular_points_per_heavy_atom = pcm_num_angular_points_per_heavy_atom
        self.pcm_num_angular_points_per_hydrogen_atom = pcm_num_angular_points_per_hydrogen_atom
        self.pcm_epsilon = pcm_epsilon
        self.pcm_x_prefactor = pcm_x_prefactor
        self.pcm_cutoff = pcm_cutoff
        self.pcm_convergence_tol = pcm_convergence_tol
        self.pcm_q = None

        self.dfk_int8_slice_count_start = dfk_int8_slice_count_start
        self.dfk_int8_modulus_count_start = dfk_int8_modulus_count_start
        self.dfk_int8_slice_count_end = dfk_int8_slice_count_end
        self.dfk_int8_modulus_count_end = dfk_int8_modulus_count_end

        if primary.natom != molecule.natom: raise RuntimeError('primary.natom != molecule.natom')
        if auxiliary.natom != molecule.natom: raise RuntimeError('auxiliary.natom != molecule.natom')
        if minao.natom != molecule.natom: raise RuntimeError('minao.natom != molecule.natom')

        self.sizes = {}
        self.scalars = {}
        self.tensors = {}

        self.is_solved = False
        self.is_converged = False

        self.gpu_xyz = GPUMatrix.from_numpy(self.molecule.xyz)
        self.gpu_Z = GPUMatrix.from_numpy(self.molecule.Z.reshape(-1,1))
        GPUMatrixUtility.scale(
            matrix=self.gpu_Z,
            scale=-1.0,
            )

    # => Cuest Objects <= #

    @memoized_property
    def cuest_primary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.primary,
            )

    @memoized_property
    def cuest_auxiliary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.auxiliary,
            )

    @memoized_property
    def cuest_xc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.xc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.xc_grid_family,
            )

    @memoized_property
    def cuest_nlc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.nlc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.nlc_grid_family,
            )

    @memoized_property
    def cuest_ao_pair_list(self):
        return CuestAOPairList(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            xyz=self.molecule.xyz,
            threshold_pq=self.threshold_pq,
            )

    @memoized_property
    def cuest_oe_int_plan(self):
        return CuestOEIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            ao_pair_list=self.cuest_ao_pair_list,
            )

    @memoized_property
    def cuest_df_int_plan(self):
        return CuestDFIntPlan(
            handle=self.cuest_handle,
            primary=self.cuest_primary,
            auxiliary=self.cuest_auxiliary,
            ao_pair_list=self.cuest_ao_pair_list,
            exchange_scale=self.cuest_xc_int_plan.exchange_scale,
            df_fitting_eigenvalue_cutoff=self.df_fitting_eigenvalue_cutoff,
            )

    @memoized_property
    def cuest_xc_int_plan(self):
        return CuestXCIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            grid=self.cuest_xc_grid,
            functional=self.xc_functional,
            xc_threshold_collocation=self.xc_threshold_collocation,
            )

    @memoized_property
    def cuest_nlc_int_plan(self):
        return CuestXCIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            grid=self.cuest_nlc_grid,
            functional=self.xc_functional,
            xc_threshold_collocation=self.nlc_threshold_collocation,
            )

    @memoized_property
    def cuest_pcm_int_plan(self):
        if self.pcm_epsilon<=1.0:
            return None
        else:
            return CuestPCMIntPlan(
                handle=self.cuest_handle,
                intPlan=self.cuest_oe_int_plan,
                num_angular_points_per_heavy_atom=self.pcm_num_angular_points_per_heavy_atom,
                num_angular_points_per_hydrogen_atom=self.pcm_num_angular_points_per_hydrogen_atom,
                epsilon=self.pcm_epsilon,
                x_prefactor=self.pcm_x_prefactor,
                Ns=self.molecule.N,
                effective_nuclear_charges=self.molecule.Z,
                cutoff=self.pcm_cutoff,
                convergence_tol=self.pcm_convergence_tol,
                )

    @property
    def do_pcm(self):
        return self.cuest_pcm_int_plan is not None

    # => Integrals <= #

    def compute_overlap(self):

        S = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        CuestOEIntCompute.overlap(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Sptr=S.pointer,
            )

        return S

    def compute_kinetic(self):

        T = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            )

        CuestOEIntCompute.kinetic(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Tptr=T.pointer,
            )

        return T

    def compute_potential(self):

        xyz = self.gpu_xyz
        q = self.gpu_Z

        V = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        CuestOEIntCompute.potential(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Vptr=V.pointer,
            ncharge=self.molecule.natom,
            xyz=xyz.pointer,
            q=q.pointer,
            )

        return V

    def compute_coulomb(
        self,
        D,
        ):

        J = D.clone()

        CuestDFIntCompute.coulomb(
            handle=self.cuest_handle,
            df_int_plan=self.cuest_df_int_plan,
            Jptr=J.pointer,
            Dptr=D.pointer,
            )

        return J

    def compute_exchange(
        self,
        Cocc,
        dfk_int8_slice_count,
        dfk_int8_modulus_count,
        ):

        K = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        if self.cuest_df_int_plan.exchange_scale == 0.0:
            K.zero()
            return K

        CuestDFIntCompute.symmetric_exchange(
            handle=self.cuest_handle,
            df_int_plan=self.cuest_df_int_plan,
            Kptr=K.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            dfk_int8_slice_count=dfk_int8_slice_count,
            dfk_int8_modulus_count=dfk_int8_modulus_count,
            )

        return K

    def compute_local_exchange_correlation(
        self,
        Cocc,
        ):

        if self.xc_functional == 0:
            return 0.0, None

        Vxc = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        Exc = CuestXCIntCompute.local_exchange_correlation(
            handle=self.cuest_handle,
            xc_int_plan=self.cuest_xc_int_plan,
            Vxcptr=Vxc.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            )

        return Exc, Vxc

    def compute_nonlocal_exchange_correlation(
        self,
        Cocc,
        ):

        if self.cuest_nlc_int_plan.vv10_scale == 0.0:
            return 0.0, None

        Vxc = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        Exc = CuestXCIntCompute.nonlocal_exchange_correlation(
            handle=self.cuest_handle,
            xc_int_plan=self.cuest_nlc_int_plan,
            Vxcptr=Vxc.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            )

        return Exc, Vxc

    def compute_pcm_energy_and_potential(
        self,
        q,
        D,
        ):

        outQ = GPUMatrix(
            nrows=self.cuest_pcm_int_plan.npoint,
            ncols=1,
            dtype=np.double,
            )

        V = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        Epcm, converged, residual = CuestPCMIntCompute.compute_pcm_energy_and_potential(
            handle=self.cuest_handle,
            pcm_int_plan=self.cuest_pcm_int_plan,
            Dptr=D.pointer,
            inQptr=q.pointer,
            outQptr=outQ.pointer,
            Vptr=V.pointer,
            )

        return Epcm, V, outQ

    # => Integral Derivatives (Gradients) <= #

    def compute_pcm_gradient(
        self,
        q,
        D,
        ):

        npoint = self.cuest_pcm_int_plan.npoint

        outQ = GPUMatrix(
            nrows=self.cuest_pcm_int_plan.npoint,
            ncols=1,
            dtype=np.double,
            )

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestPCMIntCompute.compute_pcm_gradient(
            handle=self.cuest_handle,
            pcm_int_plan=self.cuest_pcm_int_plan,
            Dptr=D.pointer,
            inQptr=q.pointer,
            outQptr=outQ.pointer,
            Gptr=G.pointer,
            )

        return G

    def compute_overlap_gradient(
        self,
        W, # Energy-weighted density matrix
        ):

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestOEIntCompute.overlap_gradient(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Gptr=G.pointer,
            Wptr=W.pointer,
            )

        return G

    def compute_kinetic_gradient(
        self,
        D, # Density matrix
        ):

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestOEIntCompute.kinetic_gradient(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Gptr=G.pointer,
            Dptr=D.pointer,
            )

        return G

    def compute_potential_gradient(
        self,
        D, # Density matrix
        ):

        Gbasis = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )
        Gcharge = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestOEIntCompute.potential_gradient(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            ncharge=self.molecule.natom,
            xyz=self.gpu_xyz.pointer,
            q=self.gpu_Z.pointer,
            Dptr=D.pointer,
            GptrBasis=Gbasis.pointer,
            GptrCharge=Gcharge.pointer,
            )

        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=Gcharge,
            y=Gbasis,
            )

        return Gbasis

    def compute_coulomb_and_exchange_gradient(
        self,
        *,
        scaleJ,
        DJ,
        scaleK,
        CoccsK,
        ):

        noccsK = [_.shape[0] for _ in CoccsK]
        assert len(CoccsK) == 1

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestDFIntCompute.coulomb_and_exchange_gradient(
            handle=self.cuest_handle,
            df_int_plan=self.cuest_df_int_plan,
            scaleJ=scaleJ,
            DJptr=DJ.pointer,
            scaleK=scaleK,
            noccsK=noccsK,
            CoccsKptr=CoccsK[0].pointer,
            Gptr=G.pointer,
            )

        return G

    def compute_local_exchange_correlation_gradient(
        self,
        Cocc,
        ):

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )
        if self.xc_functional == 0:
            G.zero()
            return G

        CuestXCIntCompute.local_exchange_correlation_gradient(
            handle=self.cuest_handle,
            xc_int_plan=self.cuest_xc_int_plan,
            Gptr=G.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            )

        return G

    def compute_nonlocal_exchange_correlation_gradient(
        self,
        Cocc,
        ):

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        if self.cuest_nlc_int_plan.vv10_scale == 0.0:
            G.zero()
            return G

        CuestXCIntCompute.nonlocal_exchange_correlation_gradient(
            handle=self.cuest_handle,
            xc_int_plan=self.cuest_nlc_int_plan,
            Gptr=G.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            )

        return G

    # => Main Solve Routines <= #

    def solve(self):

        self.is_solved = False
        self.is_converged = False

        self.header()

        # Plan initializations (to cleanly separate from iterations).  The orthogonalization
        # calls for S, so most of these are automatically instantiated in that routine.  Build
        # them here to get an idea of how long they take.
        _ = self.cuest_ao_pair_list

        _ = self.cuest_oe_int_plan

        _ = self.cuest_xc_int_plan

        start = time.time()
        _ = self.cuest_df_int_plan
        stop = time.time()

        _ = self.cuest_nlc_int_plan

        _ = self.cuest_pcm_int_plan

        self.compute_guess()

        self.compute_orthogonalization()

        self.compute_occupations()

        if self.print_level > 0:
            print('DFIntPlan Initialize Time: %11.3E [s]' % (stop - start))
            print('')

        self.solve_diis()

        self.trailer()

        self.is_solved = True

    def header(self):

        self.start_time = time.time()

        if self.print_level:
            print('==> cuEST RHF (Python) <==\n')

        if self.print_level > 1:

            print('natom  = %4d' % (self.molecule.natom))
            print('charge = %4d' % (self.charge))
            print('')

            print('Primary Basis: %s' % (self.primary_name if self.primary_name is not None else ''))
            print('')
            print(self.primary)

            print('Auxiliary Basis: %s' % (self.auxiliary_name if self.auxiliary_name is not None else ''))
            print('')
            print(self.auxiliary)

            print('MinAO Basis: %s' % (self.minao_name if self.minao_name is not None else ''))
            print('')
            print(self.minao)

            print(self.cuest_xc_int_plan)

            print('XC Grid:')
            print('')
            print(self.cuest_xc_grid)

            print('Nonlocal XC Grid:')
            print('')
            print(self.cuest_nlc_grid)

            if self.do_pcm:
                print('PCM:')
                print('')
                print(self.cuest_pcm_int_plan)

    def trailer(self):

        if self.print_level:
            print('cuEST RHF Time: %11.3E [s]\n' % (time.time() - self.start_time))

        del self.start_time

        if self.print_level:
            print('==> End cuEST RHF (Python) <==\n')

    def compute_guess(self):

        if self.print_level > 0:
            print('SAD Guess:\n')

        self.sad_guess = SADGuess.build(
            molecule=self.mol
# ... truncated ...


RHF

from .cuda_utility import CudaUtility

from .cuest_handle import CuestHandle
from .cuest_ao_basis import CuestAOBasis
from .cuest_molecular_grid import CuestMolecularGrid
from .cuest_ao_pair_list import CuestAOPairList
from .cuest_oe_int_plan import CuestOEIntPlan
from .cuest_oe_int_compute import CuestOEIntCompute
from .cuest_df_int_plan import CuestDFIntPlan
from .cuest_df_int_compute import CuestDFIntCompute
from .cuest_xc_int_plan import CuestXCIntPlan
from .cuest_xc_int_compute import CuestXCIntCompute
from .cuest_pcm_int_plan import CuestPCMIntPlan
from .cuest_pcm_int_compute import CuestPCMIntCompute

from .gpu_matrix import GPUMatrix
from .gpu_matrix_utility import GPUMatrixUtility

import time

class RHF(object):

    def __init__(
        self,
        *,
        cuest_handle : CuestHandle,
        molecule : Molecule,
        charge : int,
        xc_functional_name : str = None,
        primary : AOBasis,
        auxiliary : AOBasis,
        minao : AOBasis,
        primary_name : str = None,
        auxiliary_name : str = None,
        minao_name : str = None,
        print_level : int = 2,
        threshold_pq : float = 1.0E-12,
        threshold_canonical : float = 1.0E-6,
        df_fitting_eigenvalue_cutoff : float = 1.0e-12,
        diis_max_nvector : int = 6,
        maxiter : int = 100,
        g_convergence : float = 1.0E-6,
        xc_threshold_collocation : float = 1.0e-18,
        xc_grid_level : int = 2,
        xc_grid_family : str = "GRID",
        nlc_threshold_collocation : float = 1.0e-18,
        nlc_grid_level : int = 1,
        nlc_grid_family : str = "GRID",
        dfk_int8_slice_count_start : int = 6,
        dfk_int8_modulus_count_start : int = 5,
        dfk_int8_slice_count_end : int = 6,
        dfk_int8_modulus_count_end : int = 9,
        pcm_epsilon: float = 1.0, # 1.0 for gas phase, >1.0 for solvated calc.
        pcm_x_prefactor: float = 0.0, # 0.0 for CPCM, 0.5 for COSMO
        pcm_cutoff: float = 1.0e-10,
        pcm_convergence_tol: float = 1e-10,
        pcm_num_angular_points_per_hydrogen_atom: int = 110,
        pcm_num_angular_points_per_heavy_atom: int = 194,
        ):

        self.cuest_handle = cuest_handle

        self.molecule = molecule
        self.charge = charge

        self.xc_functional = XCFunctionalInfo.string_to_enum(xc_functional_name)

        self.primary = primary
        self.auxiliary = auxiliary
        self.minao = minao

        self.primary_name = primary_name
        self.auxiliary_name = auxiliary_name
        self.minao_name = minao_name

        self.xc_threshold_collocation = xc_threshold_collocation
        self.xc_grid_level = xc_grid_level
        self.xc_grid_family = xc_grid_family

        self.nlc_threshold_collocation = nlc_threshold_collocation
        self.nlc_grid_level = nlc_grid_level
        self.nlc_grid_family = nlc_grid_family

        self.print_level = print_level

        self.threshold_pq = threshold_pq
        self.threshold_canonical = threshold_canonical
        self.df_fitting_eigenvalue_cutoff = df_fitting_eigenvalue_cutoff
        self.diis_max_nvector = diis_max_nvector
        self.maxiter = maxiter
        self.g_convergence = g_convergence

        self.pcm_num_angular_points_per_heavy_atom = pcm_num_angular_points_per_heavy_atom
        self.pcm_num_angular_points_per_hydrogen_atom = pcm_num_angular_points_per_hydrogen_atom
        self.pcm_epsilon = pcm_epsilon
        self.pcm_x_prefactor = pcm_x_prefactor
        self.pcm_cutoff = pcm_cutoff
        self.pcm_convergence_tol = pcm_convergence_tol
        self.pcm_q = None

        self.dfk_int8_slice_count_start = dfk_int8_slice_count_start
        self.dfk_int8_modulus_count_start = dfk_int8_modulus_count_start
        self.dfk_int8_slice_count_end = dfk_int8_slice_count_end
        self.dfk_int8_modulus_count_end = dfk_int8_modulus_count_end

        if primary.natom != molecule.natom: raise RuntimeError('primary.natom != molecule.natom')
        if auxiliary.natom != molecule.natom: raise RuntimeError('auxiliary.natom != molecule.natom')
        if minao.natom != molecule.natom: raise RuntimeError('minao.natom != molecule.natom')

        self.sizes = {}
        self.scalars = {}
        self.tensors = {}

        self.is_solved = False
        self.is_converged = False

        self.gpu_xyz = GPUMatrix.from_numpy(self.molecule.xyz)
        self.gpu_Z = GPUMatrix.from_numpy(self.molecule.Z.reshape(-1,1))
        GPUMatrixUtility.scale(
            matrix=self.gpu_Z,
            scale=-1.0,
            )

    # => Cuest Objects <= #

    @memoized_property
    def cuest_primary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.primary,
            )

    @memoized_property
    def cuest_auxiliary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.auxiliary,
            )

    @memoized_property
    def cuest_xc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.xc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.xc_grid_family,
            )

    @memoized_property
    def cuest_nlc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.nlc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.nlc_grid_family,
            )

    @memoized_property
    def cuest_ao_pair_list(self):
        return CuestAOPairList(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            xyz=self.molecule.xyz,
            threshold_pq=self.threshold_pq,
            )

    @memoized_property
    def cuest_oe_int_plan(self):
        return CuestOEIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            ao_pair_list=self.cuest_ao_pair_list,
            )

    @memoized_property
    def cuest_df_int_plan(self):
        return CuestDFIntPlan(
            handle=self.cuest_handle,
            primary=self.cuest_primary,
            auxiliary=self.cuest_auxiliary,
            ao_pair_list=self.cuest_ao_pair_list,
            exchange_scale=self.cuest_xc_int_plan.exchange_scale,
            df_fitting_eigenvalue_cutoff=self.df_fitting_eigenvalue_cutoff,
            )

    @memoized_property
    def cuest_xc_int_plan(self):
        return CuestXCIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            grid=self.cuest_xc_grid,
            functional=self.xc_functional,
            xc_threshold_collocation=self.xc_threshold_collocation,
            )

    @memoized_property
    def cuest_nlc_int_plan(self):
        return CuestXCIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            grid=self.cuest_nlc_grid,
            functional=self.xc_functional,
            xc_threshold_collocation=self.nlc_threshold_collocation,
            )

    @memoized_property
    def cuest_pcm_int_plan(self):
        if self.pcm_epsilon<=1.0:
            return None
        else:
            return CuestPCMIntPlan(
                handle=self.cuest_handle,
                intPlan=self.cuest_oe_int_plan,
                num_angular_points_per_heavy_atom=self.pcm_num_angular_points_per_heavy_atom,
                num_angular_points_per_hydrogen_atom=self.pcm_num_angular_points_per_hydrogen_atom,
                epsilon=self.pcm_epsilon,
                x_prefactor=self.pcm_x_prefactor,
                Ns=self.molecule.N,
                effective_nuclear_charges=self.molecule.Z,
                cutoff=self.pcm_cutoff,
                convergence_tol=self.pcm_convergence_tol,
                )

    @property
    def do_pcm(self):
        return self.cuest_pcm_int_plan is not None

    # => Integrals <= #

    def compute_overlap(self):

        S = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        CuestOEIntCompute.overlap(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Sptr=S.pointer,
            )

        return S

    def compute_kinetic(self):

        T = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            )

        CuestOEIntCompute.kinetic(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Tptr=T.pointer,
            )

        return T

    def compute_potential(self):

        xyz = self.gpu_xyz
        q = self.gpu_Z

        V = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        CuestOEIntCompute.potential(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Vptr=V.pointer,
            ncharge=self.molecule.natom,
            xyz=xyz.pointer,
            q=q.pointer,
            )

        return V

    def compute_coulomb(
        self,
        D,
        ):

        J = D.clone()

        CuestDFIntCompute.coulomb(
            handle=self.cuest_handle,
            df_int_plan=self.cuest_df_int_plan,
            Jptr=J.pointer,
            Dptr=D.pointer,
            )

        return J

    def compute_exchange(
        self,
        Cocc,
        dfk_int8_slice_count,
        dfk_int8_modulus_count,
        ):

        K = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        if self.cuest_df_int_plan.exchange_scale == 0.0:
            K.zero()
            return K

        CuestDFIntCompute.symmetric_exchange(
            handle=self.cuest_handle,
            df_int_plan=self.cuest_df_int_plan,
            Kptr=K.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            dfk_int8_slice_count=dfk_int8_slice_count,
            dfk_int8_modulus_count=dfk_int8_modulus_count,
            )

        return K

    def compute_local_exchange_correlation(
        self,
        Cocc,
        ):

        if self.xc_functional == 0:
            return 0.0, None

        Vxc = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        Exc = CuestXCIntCompute.local_exchange_correlation(
            handle=self.cuest_handle,
            xc_int_plan=self.cuest_xc_int_plan,
            Vxcptr=Vxc.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            )

        return Exc, Vxc

    def compute_nonlocal_exchange_correlation(
        self,
        Cocc,
        ):

        if self.cuest_nlc_int_plan.vv10_scale == 0.0:
            return 0.0, None

        Vxc = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        Exc = CuestXCIntCompute.nonlocal_exchange_correlation(
            handle=self.cuest_handle,
            xc_int_plan=self.cuest_nlc_int_plan,
            Vxcptr=Vxc.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            )

        return Exc, Vxc

    def compute_pcm_energy_and_potential(
        self,
        q,
        D,
        ):

        outQ = GPUMatrix(
            nrows=self.cuest_pcm_int_plan.npoint,
            ncols=1,
            dtype=np.double,
            )

        V = GPUMatrix(
            nrows=self.primary.nao,
            ncols=self.primary.nao,
            dtype=np.double,
            )

        Epcm, converged, residual = CuestPCMIntCompute.compute_pcm_energy_and_potential(
            handle=self.cuest_handle,
            pcm_int_plan=self.cuest_pcm_int_plan,
            Dptr=D.pointer,
            inQptr=q.pointer,
            outQptr=outQ.pointer,
            Vptr=V.pointer,
            )

        return Epcm, V, outQ

    # => Integral Derivatives (Gradients) <= #

    def compute_pcm_gradient(
        self,
        q,
        D,
        ):

        npoint = self.cuest_pcm_int_plan.npoint

        outQ = GPUMatrix(
            nrows=self.cuest_pcm_int_plan.npoint,
            ncols=1,
            dtype=np.double,
            )

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestPCMIntCompute.compute_pcm_gradient(
            handle=self.cuest_handle,
            pcm_int_plan=self.cuest_pcm_int_plan,
            Dptr=D.pointer,
            inQptr=q.pointer,
            outQptr=outQ.pointer,
            Gptr=G.pointer,
            )

        return G

    def compute_overlap_gradient(
        self,
        W, # Energy-weighted density matrix
        ):

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestOEIntCompute.overlap_gradient(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Gptr=G.pointer,
            Wptr=W.pointer,
            )

        return G

    def compute_kinetic_gradient(
        self,
        D, # Density matrix
        ):

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestOEIntCompute.kinetic_gradient(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            Gptr=G.pointer,
            Dptr=D.pointer,
            )

        return G

    def compute_potential_gradient(
        self,
        D, # Density matrix
        ):

        Gbasis = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )
        Gcharge = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestOEIntCompute.potential_gradient(
            handle=self.cuest_handle,
            oe_int_plan=self.cuest_oe_int_plan,
            ncharge=self.molecule.natom,
            xyz=self.gpu_xyz.pointer,
            q=self.gpu_Z.pointer,
            Dptr=D.pointer,
            GptrBasis=Gbasis.pointer,
            GptrCharge=Gcharge.pointer,
            )

        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=Gcharge,
            y=Gbasis,
            )

        return Gbasis

    def compute_coulomb_and_exchange_gradient(
        self,
        *,
        scaleJ,
        DJ,
        scaleK,
        CoccsK,
        ):

        noccsK = [_.shape[0] for _ in CoccsK]
        assert len(CoccsK) == 1

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        CuestDFIntCompute.coulomb_and_exchange_gradient(
            handle=self.cuest_handle,
            df_int_plan=self.cuest_df_int_plan,
            scaleJ=scaleJ,
            DJptr=DJ.pointer,
            scaleK=scaleK,
            noccsK=noccsK,
            CoccsKptr=CoccsK[0].pointer,
            Gptr=G.pointer,
            )

        return G

    def compute_local_exchange_correlation_gradient(
        self,
        Cocc,
        ):

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )
        if self.xc_functional == 0:
            G.zero()
            return G

        CuestXCIntCompute.local_exchange_correlation_gradient(
            handle=self.cuest_handle,
            xc_int_plan=self.cuest_xc_int_plan,
            Gptr=G.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            )

        return G

    def compute_nonlocal_exchange_correlation_gradient(
        self,
        Cocc,
        ):

        G = GPUMatrix(
            nrows=self.primary.natom,
            ncols=3,
            dtype=np.double,
            )

        if self.cuest_nlc_int_plan.vv10_scale == 0.0:
            G.zero()
            return G

        CuestXCIntCompute.nonlocal_exchange_correlation_gradient(
            handle=self.cuest_handle,
            xc_int_plan=self.cuest_nlc_int_plan,
            Gptr=G.pointer,
            nocc=Cocc.shape[0],
            Coccptr=Cocc.pointer,
            )

        return G

    # => Main Solve Routines <= #

    def solve(self):

        self.is_solved = False
        self.is_converged = False

        self.header()

        # Plan initializations (to cleanly separate from iterations).  The orthogonalization
        # calls for S, so most of these are automatically instantiated in that routine.  Build
        # them here to get an idea of how long they take.
        _ = self.cuest_ao_pair_list

        _ = self.cuest_oe_int_plan

        _ = self.cuest_xc_int_plan

        start = time.time()
        _ = self.cuest_df_int_plan
        stop = time.time()

        _ = self.cuest_nlc_int_plan

        _ = self.cuest_pcm_int_plan

        self.compute_guess()

        self.compute_orthogonalization()

        self.compute_occupations()

        if self.print_level > 0:
            print('DFIntPlan Initialize Time: %11.3E [s]' % (stop - start))
            print('')

        self.solve_diis()

        self.trailer()

        self.is_solved = True

    def header(self):

        self.start_time = time.time()

        if self.print_level:
            print('==> cuEST RHF (Python) <==\n')

        if self.print_level > 1:

            print('natom  = %4d' % (self.molecule.natom))
            print('charge = %4d' % (self.charge))
            print('')

            print('Primary Basis: %s' % (self.primary_name if self.primary_name is not None else ''))
            print('')
            print(self.primary)

            print('Auxiliary Basis: %s' % (self.auxiliary_name if self.auxiliary_name is not None else ''))
            print('')
            print(self.auxiliary)

            print('MinAO Basis: %s' % (self.minao_name if self.minao_name is not None else ''))
            print('')
            print(self.minao)

            print(self.cuest_xc_int_plan)

            print('XC Grid:')
            print('')
            print(self.cuest_xc_grid)

            print('Nonlocal XC Grid:')
            print('')
            print(self.cuest_nlc_grid)

            if self.do_pcm:
                print('PCM:')
                print('')
                print(self.cuest_pcm_int_plan)

    def trailer(self):

        if self.print_level:
            print('cuEST RHF Time: %11.3E [s]\n' % (time.time() - self.start_time))

        del self.start_time

        if self.print_level:
            print('==> End cuEST RHF (Python) <==\n')

    def compute_guess(self):

        if self.print_level > 0:
            print('SAD Guess:\n')

        self.sad_guess = SADGuess.build(
            molecule=self.molecule,
            primary=self.primary,
            minao=self.minao,
            )

        # For now this will be size (nminao, nao), which is usually larger than
        # (nocc, nao) due to fractional occupations in SAD
        self.tensors['Cocc'] = GPUMatrix.from_numpy(self.sad_guess.compute_Cocc())

        # For now this is not a pure state, and corresponds to the neutral SAD
        # guess
        self.tensors['D'] = GPUMatrix.from_numpy(self.sad_guess.compute_Docc())

    def compute_orthogonalization(self):

        if self.print_level > 0:
            print('Orthogonalization:\n')

        S = self.compute_overlap()

        s, U = GPUMatrixUtility.eigh(matrix=S)
        s = s.to_numpy().reshape(-1)
        U = U.to_numpy()

        # Canonical orthogonalization is traditionally done on an absolute eigenvalue threshold basis
        sind = s > self.threshold_canonical
# ... truncated ...


from .cuest_handle import CuestHandle
from .cuest_ao_basis import CuestAOBasis
from .cuest_molecular_grid import CuestMolecularGrid
from .cuest_ao_pair_list import CuestAOPairList
from .cuest_oe_int_plan import CuestOEIntPlan
from .cuest_oe_int_compute import CuestOEIntCompute
from .cuest_df_int_plan import CuestDFIntPlan
from .cuest_df_int_compute import CuestDFIntCompute
from .cuest_xc_int_plan import CuestXCIntPlan
from .cuest_xc_int_compute import CuestXCIntCompute
from .cuest_pcm_int_plan import CuestPCMIntPlan
from .cuest_pcm_int_compute import CuestPCMIntCompute

from .gpu_matrix import GPUMatrix
from .gpu_matrix_utility import GPUMatrixUtility

import time

class RHF(object):

    def __init__(
        self,
        *,
        cuest_handle : CuestHandle,
        molecule : Molecule,
        charge : int,
        xc_functional_name : str = None,
        primary : AOBasis,
        auxiliary : AOBasis,
        minao : AOBasis,
        primary_name : str = None,
        auxiliary_name : str = None,
        minao_name : str = None,
        print_level : int = 2,
        threshold_pq : float = 1.0E-12,
        threshold_canonical : float = 1.0E-6,
        df_fitting_eigenvalue_cutoff : float = 1.0e-12,
        diis_max_nvector : int = 6,
        maxiter : int = 100,
        g_convergence : float = 1.0E-6,
        xc_threshold_collocation : float = 1.0e-18,
        xc_grid_level : int = 2,
        xc_grid_family : str = "GRID",
        nlc_threshold_collocation : float = 1.0e-18,
        nlc_grid_level : int = 1,
        nlc_grid_family : str = "GRID",
        dfk_int8_slice_count_start : int = 6,
        dfk_int8_modulus_count_start : int = 5,
        dfk_int8_slice_count_end : int = 6,
        dfk_int8_modulus_count_end : int = 9,
        pcm_epsilon: float = 1.0, # 1.0 for gas phase, >1.0 for solvated calc.
        pcm_x_prefactor: float = 0.0, # 0.0 for CPCM, 0.5 for COSMO
        pcm_cutoff: float = 1.0e-10,
        pcm_convergence_tol: float = 1e-10,
        pcm_num_angular_points_per_hydrogen_atom: int = 110,
        pcm_num_angular_points_per_heavy_atom: int = 194,
        ):

        self.cuest_handle = cuest_handle

        self.molecule = molecule
        self.charge = charge

        self.xc_functional = XCFunctionalInfo.string_to_enum(xc_functional_name)

        self.primary = primary
        self.auxiliary = auxiliary
        self.minao = minao

        self.primary_name = primary_name
        self.auxiliary_name = auxiliary_name
        self.minao_name = minao_name

        self.xc_threshold_collocation = xc_threshold_collocation
        self.xc_grid_level = xc_grid_level
        self.xc_grid_family = xc_grid_family

        self.nlc_threshold_collocation = nlc_threshold_collocation
        self.nlc_grid_level = nlc_grid_level
        self.nlc_grid_family = nlc_grid_family

        self.print_level = print_level

        self.threshold_pq = threshold_pq
        self.threshold_canonical = threshold_canonical
        self.df_fitting_eigenvalue_cutoff = df_fitting_eigenvalue_cutoff
        self.diis_max_nvector = diis_max_nvector
        self.maxiter = maxiter
        self.g_convergence = g_convergence

        self.pcm_num_angular_points_per_heavy_atom = pcm_num_angular_points_per_heavy_atom
        self.pcm_num_angular_points_per_hydrogen_atom = pcm_num_angular_points_per_hydrogen_atom
        self.pcm_epsilon = pcm_epsilon
        self.pcm_x_prefactor = pcm_x_prefactor
        self.pcm_cutoff = pcm_cutoff
        self.pcm_convergence_tol = pcm_convergence_tol
        self.pcm_q = None

        self.dfk_int8_slice_count_start = dfk_int8_slice_count_start
        self.dfk_int8_modulus_count_start = dfk_int8_modulus_count_start
        self.dfk_int8_slice_count_end = dfk_int8_slice_count_end
        self.dfk_int8_modulus_count_end = dfk_int8_modulus_count_end

        if primary.natom != molecule.natom: raise RuntimeError('primary.natom != molecule.natom')
        if auxiliary.natom != molecule.natom: raise RuntimeError('auxiliary.natom != molecule.natom')
        if minao.natom != molecule.natom: raise RuntimeError('minao.natom != molecule.natom')

        self.sizes = {}
        self.scalars = {}
        self.tensors = {}

        self.is_solved = False
        self.is_converged = False

        self.gpu_xyz = GPUMatrix.from_numpy(self.molecule.xyz)
        self.gpu_Z = GPUMatrix.from_numpy(self.molecule.Z.reshape(-1,1))
        GPUMatrixUtility.scale(
            matrix=self.gpu_Z,
            scale=-1.0,
            )

    # => Cuest Objects <= #

    @memoized_property
    def cuest_primary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.primary,
            )

    @memoized_property
    def cuest_auxiliary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.auxiliary,
            )

    @memoized_property
    def cuest_xc_grid(self):
        return CuestMolecularGrid(


        if auxiliary.natom != molecule.natom: raise RuntimeError('auxiliary.natom != molecule.natom')
        if minao.natom != molecule.natom: raise RuntimeError('minao.natom != molecule.natom')

        self.sizes = {}
        self.scalars = {}
        self.tensors = {}

        self.is_solved = False
        self.is_converged = False

        self.gpu_xyz = GPUMatrix.from_numpy(self.molecule.xyz)
        self.gpu_Z = GPUMatrix.from_numpy(self.molecule.Z.reshape(-1,1))
        GPUMatrixUtility.scale(
            matrix=self.gpu_Z,
            scale=-1.0,
            )

    # => Cuest Objects <= #

    @memoized_property
    def cuest_primary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.primary,
            )

    @memoized_property
    def cuest_auxiliary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.auxiliary,
            )

    @memoized_property
    def cuest_xc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.xc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.xc_grid_family,
            )

    @memoized_property
    def cuest_nlc_grid(self):


        self.is_solved = False
        self.is_converged = False

        self.gpu_xyz = GPUMatrix.from_numpy(self.molecule.xyz)
        self.gpu_Z = GPUMatrix.from_numpy(self.molecule.Z.reshape(-1,1))
        GPUMatrixUtility.scale(
            matrix=self.gpu_Z,
            scale=-1.0,
            )

    # => Cuest Objects <= #

    @memoized_property
    def cuest_primary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.primary,
            )

    @memoized_property
    def cuest_auxiliary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.auxiliary,
            )

    @memoized_property
    def cuest_xc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.xc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.xc_grid_family,
            )

    @memoized_property
    def cuest_nlc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.nlc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.nlc_grid_family,
            )


            scale=-1.0,
            )

    # => Cuest Objects <= #

    @memoized_property
    def cuest_primary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.primary,
            )

    @memoized_property
    def cuest_auxiliary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.auxiliary,
            )

    @memoized_property
    def cuest_xc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.xc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.xc_grid_family,
            )

    @memoized_property
    def cuest_nlc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.nlc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.nlc_grid_family,
            )

    @memoized_property
    def cuest_ao_pair_list(self):
        return CuestAOPairList(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            xyz=self.molecule.xyz,
            threshold_pq=self.threshold_pq,
            )


            )

    @memoized_property
    def cuest_auxiliary(self):
        return CuestAOBasis(
            handle=self.cuest_handle,
            basis=self.auxiliary,
            )

    @memoized_property
    def cuest_xc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.xc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.xc_grid_family,
            )

    @memoized_property
    def cuest_nlc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.nlc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.nlc_grid_family,
            )

    @memoized_property
    def cuest_ao_pair_list(self):
        return CuestAOPairList(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            xyz=self.molecule.xyz,
            threshold_pq=self.threshold_pq,
            )

    @memoized_property
    def cuest_oe_int_plan(self):
        return CuestOEIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            ao_pair_list=self.cuest_ao_pair_list,
            )

    @memoized_property
    def cuest_df_int_plan(self):


    def cuest_xc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.xc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.xc_grid_family,
            )

    @memoized_property
    def cuest_nlc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.nlc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.nlc_grid_family,
            )

    @memoized_property
    def cuest_ao_pair_list(self):
        return CuestAOPairList(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            xyz=self.molecule.xyz,
            threshold_pq=self.threshold_pq,
            )

    @memoized_property
    def cuest_oe_int_plan(self):
        return CuestOEIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            ao_pair_list=self.cuest_ao_pair_list,
            )

    @memoized_property
    def cuest_df_int_plan(self):
        return CuestDFIntPlan(
            handle=self.cuest_handle,
            primary=self.cuest_primary,
            auxiliary=self.cuest_auxiliary,
            ao_pair_list=self.cuest_ao_pair_list,
            exchange_scale=self.cuest_xc_int_plan.exchange_scale,
            df_fitting_eigenvalue_cutoff=self.df_fitting_eigenvalue_cutoff,
            )


    @memoized_property
    def cuest_nlc_grid(self):
        return CuestMolecularGrid(
            handle=self.cuest_handle,
            grid_level=self.nlc_grid_level,
            xyz=self.molecule.xyz,
            Ns=self.molecule.N,
            family=self.nlc_grid_family,
            )

    @memoized_property
    def cuest_ao_pair_list(self):
        return CuestAOPairList(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            xyz=self.molecule.xyz,
            threshold_pq=self.threshold_pq,
            )

    @memoized_property
    def cuest_oe_int_plan(self):
        return CuestOEIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            ao_pair_list=self.cuest_ao_pair_list,
            )

    @memoized_property
    def cuest_df_int_plan(self):
        return CuestDFIntPlan(
            handle=self.cuest_handle,
            primary=self.cuest_primary,
            auxiliary=self.cuest_auxiliary,
            ao_pair_list=self.cuest_ao_pair_list,
            exchange_scale=self.cuest_xc_int_plan.exchange_scale,
            df_fitting_eigenvalue_cutoff=self.df_fitting_eigenvalue_cutoff,
            )

    @memoized_property
    def cuest_xc_int_plan(self):
        return CuestXCIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            grid=self.cuest_xc_grid,
            functional=self.xc_functional,
            xc_threshold_collocation=self.xc_threshold_collocation,


            )

    @memoized_property
    def cuest_ao_pair_list(self):
        return CuestAOPairList(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            xyz=self.molecule.xyz,
            threshold_pq=self.threshold_pq,
            )

    @memoized_property
    def cuest_oe_int_plan(self):
        return CuestOEIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            ao_pair_list=self.cuest_ao_pair_list,
            )

    @memoized_property
    def cuest_df_int_plan(self):
        return CuestDFIntPlan(
            handle=self.cuest_handle,
            primary=self.cuest_primary,
            auxiliary=self.cuest_auxiliary,
            ao_pair_list=self.cuest_ao_pair_list,
            exchange_scale=self.cuest_xc_int_plan.exchange_scale,
            df_fitting_eigenvalue_cutoff=self.df_fitting_eigenvalue_cutoff,
            )

    @memoized_property
    def cuest_xc_int_plan(self):
        return CuestXCIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            grid=self.cuest_xc_grid,
            functional=self.xc_functional,
            xc_threshold_collocation=self.xc_threshold_collocation,
            )

    @memoized_property
    def cuest_nlc_int_plan(self):
        return CuestXCIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            grid=self.cuest_nlc_grid,
            functional=self.xc_functional,
            xc_threshold_collocation=self.nlc_threshold_collocation,
            )


    @memoized_property
    def cuest_oe_int_plan(self):
        return CuestOEIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            ao_pair_list=self.cuest_ao_pair_list,
            )

    @memoized_property
    def cuest_df_int_plan(self):
        return CuestDFIntPlan(
            handle=self.cuest_handle,
            primary=self.cuest_primary,
            auxiliary=self.cuest_auxiliary,
            ao_pair_list=self.cuest_ao_pair_list,
            exchange_scale=self.cuest_xc_int_plan.exchange_scale,
            df_fitting_eigenvalue_cutoff=self.df_fitting_eigenvalue_cutoff,
            )

    @memoized_property
    def cuest_xc_int_plan(self):
        return CuestXCIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            grid=self.cuest_xc_grid,
            functional=self.xc_functional,
            xc_threshold_collocation=self.xc_threshold_collocation,
            )

    @memoized_property
    def cuest_nlc_int_plan(self):
        return CuestXCIntPlan(
            handle=self.cuest_handle,
            basis=self.cuest_primary,
            grid=self.cuest_nlc_grid,
            functional=self.xc_functional,
            xc_threshold_collocation=self.nlc_threshold_collocation,
            )

    @memoized_property
    def cuest_pcm_int_plan(self):
        if self.pcm_epsilon<=1.0:
            return None
        else:
            return CuestPCMIntPlan(
                handle=self.cuest_handle,
                intPlan=self.cuest_oe_int_plan,
                num_angular_points_per_heavy_atom=self.pcm_num_angular_points_per_heavy_atom,