        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 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,


    @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


            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


    # => 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,
            )


            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,


        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,


            )

        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],


            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],


        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(


        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,


        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


            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_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,


    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(


        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],


        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,


        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()


solve

            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('')


header

        _ = 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,


trailer

            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())


            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

        s2 = s[sind]
        U2 = U[:, sind]

        X = np.einsum('ij,j->ji', U2, s2**(-0.5))


    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

        s2 = s[sind]
        U2 = U[:, sind]

        X = np.einsum('ij,j->ji', U2, s2**(-0.5))

        # This should be numerically zero:
        # d = np.max(np.abs(np.dot(np.dot(X, S), X.T) - np.eye(X.shape[0])))

        self.tensors['S'] = S
        self.tensors['X'] = GPUMatrix.from_numpy(X)

        if self.print_level > 0:
            print('threshold_canonical = %11.3E' % (self.threshold_canonical))
            print('nao                 = %11d' % (X.shape[1]))
            print('nmo                 = %11d' % (X.shape[0]))
            print('ndiscard            = %11d' % (X.shape[1] - X.shape[0]))
            print('')

    def compute_occupations(self):

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

        Q = np.sum(self.molecule.Z) - self.charge
        nocc = int(Q / 2)

        if 2 * nocc != Q: raise RuntimeError('molecule is not closed shell')

        nvir = self.tensors['X'].shape[0] - nocc

        self.sizes['nocc'] = nocc
        self.sizes['nvir'] = nvir

        if self.print_level > 0:
            print('%-5s = %5d' % ('nocc', self.sizes['nocc']))
            print('%-5s = %5d' % ('nvir', self.sizes['nvir']))
            print('')


        sind = s > self.threshold_canonical

        s2 = s[sind]
        U2 = U[:, sind]

        X = np.einsum('ij,j->ji', U2, s2**(-0.5))

        # This should be numerically zero:
        # d = np.max(np.abs(np.dot(np.dot(X, S), X.T) - np.eye(X.shape[0])))

        self.tensors['S'] = S
        self.tensors['X'] = GPUMatrix.from_numpy(X)

        if self.print_level > 0:
            print('threshold_canonical = %11.3E' % (self.threshold_canonical))
            print('nao                 = %11d' % (X.shape[1]))
            print('nmo                 = %11d' % (X.shape[0]))
            print('ndiscard            = %11d' % (X.shape[1] - X.shape[0]))
            print('')

    def compute_occupations(self):

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

        Q = np.sum(self.molecule.Z) - self.charge
        nocc = int(Q / 2)

        if 2 * nocc != Q: raise RuntimeError('molecule is not closed shell')

        nvir = self.tensors['X'].shape[0] - nocc

        self.sizes['nocc'] = nocc
        self.sizes['nvir'] = nvir

        if self.print_level > 0:
            print('%-5s = %5d' % ('nocc', self.sizes['nocc']))
            print('%-5s = %5d' % ('nvir', self.sizes['nvir']))
            print('')

    def compute_Enuc(self):

        xyz = self.molecule.xyz
        Z = self.molecule.Z

        Enuc = 0.0
        for A in range(xyz.shape[0]):
            xyzA = xyz[A, :]
            ZA = Z[A]
            for B in range(xyz.shape[0]):
                if A == B: continue
                xyzB = xyz[B, :]
                ZB = Z[B]
                rAB = np.sqrt(np.sum((xyzA - xyzB)**2))
                Enuc += 0.5 * ZA * ZB / rAB
        return Enuc

    def compute_Enuc_gradient(self):


    def compute_occupations(self):

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

        Q = np.sum(self.molecule.Z) - self.charge
        nocc = int(Q / 2)

        if 2 * nocc != Q: raise RuntimeError('molecule is not closed shell')

        nvir = self.tensors['X'].shape[0] - nocc

        self.sizes['nocc'] = nocc
        self.sizes['nvir'] = nvir

        if self.print_level > 0:
            print('%-5s = %5d' % ('nocc', self.sizes['nocc']))
            print('%-5s = %5d' % ('nvir', self.sizes['nvir']))
            print('')

    def compute_Enuc(self):

        xyz = self.molecule.xyz
        Z = self.molecule.Z

        Enuc = 0.0
        for A in range(xyz.shape[0]):
            xyzA = xyz[A, :]
            ZA = Z[A]
            for B in range(xyz.shape[0]):
                if A == B: continue
                xyzB = xyz[B, :]
                ZB = Z[B]
                rAB = np.sqrt(np.sum((xyzA - xyzB)**2))
                Enuc += 0.5 * ZA * ZB / rAB
        return Enuc

    def compute_Enuc_gradient(self):

        xyz = self.molecule.xyz
        Z = self.molecule.Z

        Gnuc = np.zeros((self.molecule.natom, 3))
        for A in range(xyz.shape[0]):
            xyzA = xyz[A, :]
            ZA = Z[A]
            for B in range(xyz.shape[0]):
                if A == B: continue
                xyzB = xyz[B, :]
                ZB = Z[B]
                rAB = np.sqrt(np.sum((xyzA - xyzB)**2))
                Gnuc[A, :] -= ZA * ZB / rAB**3 * (xyzA - xyzB)
        return GPUMatrix.from_numpy(Gnuc)

    def find_slice_count(
        self,


            print('%-5s = %5d' % ('nvir', self.sizes['nvir']))
            print('')

    def compute_Enuc(self):

        xyz = self.molecule.xyz
        Z = self.molecule.Z

        Enuc = 0.0
        for A in range(xyz.shape[0]):
            xyzA = xyz[A, :]
            ZA = Z[A]
            for B in range(xyz.shape[0]):
                if A == B: continue
                xyzB = xyz[B, :]
                ZB = Z[B]
                rAB = np.sqrt(np.sum((xyzA - xyzB)**2))
                Enuc += 0.5 * ZA * ZB / rAB
        return Enuc

    def compute_Enuc_gradient(self):

        xyz = self.molecule.xyz
        Z = self.molecule.Z

        Gnuc = np.zeros((self.molecule.natom, 3))
        for A in range(xyz.shape[0]):
            xyzA = xyz[A, :]
            ZA = Z[A]
            for B in range(xyz.shape[0]):
                if A == B: continue
                xyzB = xyz[B, :]
                ZB = Z[B]
                rAB = np.sqrt(np.sum((xyzA - xyzB)**2))
                Gnuc[A, :] -= ZA * ZB / rAB**3 * (xyzA - xyzB)
        return GPUMatrix.from_numpy(Gnuc)

    def find_slice_count(
        self,
        *,
        slice_start,
        slice_end,
        dG_start,
        dG_end,
        dG,
        ):

        if dG >= dG_start:
            return slice_start

        if dG <= dG_end:
            return slice_end

        log_dG_start = np.log10(dG_start)
        log_dG_end = np.log10(dG_end)
        log_dG = np.log10(dG)


                Enuc += 0.5 * ZA * ZB / rAB
        return Enuc

    def compute_Enuc_gradient(self):

        xyz = self.molecule.xyz
        Z = self.molecule.Z

        Gnuc = np.zeros((self.molecule.natom, 3))
        for A in range(xyz.shape[0]):
            xyzA = xyz[A, :]
            ZA = Z[A]
            for B in range(xyz.shape[0]):
                if A == B: continue
                xyzB = xyz[B, :]
                ZB = Z[B]
                rAB = np.sqrt(np.sum((xyzA - xyzB)**2))
                Gnuc[A, :] -= ZA * ZB / rAB**3 * (xyzA - xyzB)
        return GPUMatrix.from_numpy(Gnuc)

    def find_slice_count(
        self,
        *,
        slice_start,
        slice_end,
        dG_start,
        dG_end,
        dG,
        ):

        if dG >= dG_start:
            return slice_start

        if dG <= dG_end:
            return slice_end

        log_dG_start = np.log10(dG_start)
        log_dG_end = np.log10(dG_end)
        log_dG = np.log10(dG)

        t = (log_dG - log_dG_start) / (log_dG_end - log_dG_start)

        return int(round(slice_start + t * ( slice_end - slice_start)))

    def solve_diis(self):

        if self.print_level > 0:
            print('=> Solve DIIS <=\n')

        # Threshold PQ (odd place to report this, but so be it)

        if self.print_level > 0:
            print('threshold_pq = %11.3E' % self.threshold_pq)
            print('')

        # PCM Initialization

        if self.do_pcm:
            self.pcm_q = GPUMatrix(
                nrows=self.cuest_pcm_int_plan.npoint,
                ncols=1,
                dtype=np.double,


        slice_end,
        dG_start,
        dG_end,
        dG,
        ):

        if dG >= dG_start:
            return slice_start

        if dG <= dG_end:
            return slice_end

        log_dG_start = np.log10(dG_start)
        log_dG_end = np.log10(dG_end)
        log_dG = np.log10(dG)

        t = (log_dG - log_dG_start) / (log_dG_end - log_dG_start)

        return int(round(slice_start + t * ( slice_end - slice_start)))

    def solve_diis(self):

        if self.print_level > 0:
            print('=> Solve DIIS <=\n')

        # Threshold PQ (odd place to report this, but so be it)

        if self.print_level > 0:
            print('threshold_pq = %11.3E' % self.threshold_pq)
            print('')

        # PCM Initialization

        if self.do_pcm:
            self.pcm_q = GPUMatrix(
                nrows=self.cuest_pcm_int_plan.npoint,
                ncols=1,
                dtype=np.double,
                initialize=True,
                )
            self.pcm_q.zero()

        # External Nuclear-repulsion energy

        Enuc = self.compute_Enuc()
        self.scalars['Enuc'] = Enuc

        if self.print_level > 0:
            print('Enuc = %24.16E' % (Enuc))
            print('')

        S = self.tensors['S']
        X = self.tensors['X']

        # Core Hamiltonian

        T = self.compute_kinetic()
        V = self.compute_potential()

        # NOTE: This is typically where other static external potential terms
        # are added, e.g., ECP, QM/MM electrostatics, external perturbations,
        # etc.

        H = T.clone()
        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=V,
            y=H,
            )

        diis = DIIS(max_nvector=self.diis_max_nvector)

        if self.print_level > 1:
            print('DIIS:')
            print('max nvector = %11d' % self.diis_max_nvector)
            print('')

        if self.print_level > 1:
            print('Convergence Options:')
            print('max iterations = %11d' % self.maxiter)
            print('g convergence  = %11.3E' % self.g_convergence)
            print('')

        # Main SCF Iterative Loop

        start = time.time()
        self.is_converged = False
        Eold = 0.0
        E = 0.0
        dG = 1.0
        print('%-4s: %24s %11s %11s %8s' % ('Iter', 'Energy', 'dE', 'dG', 'Time[s]'))
        for iteration in range(self.maxiter):

            D = self.tensors['D']
            Cocc = self.tensors['Cocc']

            # Find slice count
            dfk_int8_slice_count = self.find_slice_count(
                slice_start=self.dfk_int8_slice_count_start,
                slice_end=self.dfk_int8_slice_count_end,
                dG_start=1.0,
                dG_end=self.g_convergence,
                dG=dG,
                )
            dfk_int8_modulus_count = self.find_slice_count(
                slice_start=self.dfk_int8_modulus_count_start,
                slice_end=self.dfk_int8_modulus_count_end,
                dG_start=1.0,
                dG_end=self.g_convergence,
                dG=dG,
                )

            # Fock Matrix

            J = self.compute_coulomb(D=D)

            K = self.compute_exchange(
                Cocc=Cocc,
                dfk_int8_slice_count=dfk_int8_slice_count,
                dfk_int8_modulus_count=dfk_int8_modulus_count,
                )

            Exc, Vxc = self.compute_local_exchange_correlation(Cocc=Cocc)

            Enlc, Vnlc = self.compute_nonlocal_exchange_correlation(Cocc=Cocc)

            # NOTE: This is typically where other electron-dependent Fock
            # matrix terms are added, e.g., PCM, wK.

            F = H.clone()
            GPUMatrixUtility.daxpy(
                alpha=2.0,
                x=J,
                y=F,
                )
            GPUMatrixUtility.daxpy(
                alpha=-1.0,
                x=K,
                y=F,
                )

            # SCF Energy

            E = 0.0
            E += Enuc
            E += 1.0 * GPUMatrixUtility.ddot(
                x=D,
                y=H,
                )
            E += 1.0 * GPUMatrixUtility.ddot(
                x=D,
                y=F,
                )

            # Include PCM
            if self.do_pcm:
                # PCM expects the total density, so temporarily scale by 2.0
                GPUMatrixUtility.scale(
                    matrix=D,
                    scale=2.0,
                    )
                Epcm, Vpcm, q_out = self.compute_pcm_energy_and_potential(
                    q=self.pcm_q,
                    D=D)
                GPUMatrixUtility.scale(
                    matrix=D,
                    scale=0.5,
                    )

                self.pcm_q = q_out

                E += Epcm
                GPUMatrixUtility.daxpy(
                    alpha=1.0,
                    x=Vpcm,
                    y=F,
                    )

            E += Exc
            E += Enlc
            self.scalars['Escf'] = E

            # Add Vxc to the Fock matrix after calculating the energy

            if Vxc is not None:
                GPUMatrixUtility.daxpy(
                    alpha=1.0,
                    x=Vxc,
                    y=F,
                    )
            if Vnlc is not None:
                GPUMatrixUtility.daxpy(
                    alpha=1.0,
                    x=Vnlc,
                    y=F,
                    )

            dE = E - Eold
            Eold = E

            # Orbital Gradient (zero at convergence)
            # G = XSDFX' - XFDSX'

            # G = np.dot(np.dot(np.dot(np.dot(X, S), D), F), X.T)
            # Faster in large basis sets

            L1 = GPUMatrixUtility.matrix_multiply(
                mat1=Cocc,
                mat2=S,
                transpose1=False,
                transpose2=False,
                )
            L = GPUMatrixUtility.matrix_multiply(
                mat1=L1,
                mat2=X,
                transpose1=False,
                transpose2=True,
                )
            R1 = GPUMatrixUtility.matrix_multiply(
                mat1=Cocc,
                mat2=F,
                transpose1=False,
                transpose2=False,
                )
            R = GPUMatrixUtility.matrix_multiply(
                mat1=R1,
                mat2=X,
                transpose1=False,
                transpose2=True,
                )

            G = GPUMatrixUtility.matrix_multiply(
                mat1=L,
                mat2=R,
                transpose1=True,
                transpose2=False,
                )
            G = GPUMatrixUtility.dgeam(
                mat1=G,
                alpha=1.0,
                transpose1=False,
                mat2=G,
                beta=-1.0,
                transpose2=True,
                )

            G_np = G.to_numpy()

            dG = np.max(np.abs(G_np))

            # Convergence Trace

            stop = time.time()
            if self.print_level:
                print('%-4d: %24.16E %11.3E %11.3E %8.3f (%2d moduli, %2d slices)' % (iteration, E, dE, dG, stop-start, dfk_int8_modulus_count, dfk_int8_slice_count))
            start = stop

            # Convergence Check

            if iteration > 0 and dG < self.g_convergence:
                self.is_converged = True
                break

            # This ensures that we do not extrapolate/diagonalize if maxiter is reached
            if iteration >= self.maxiter - 1:
                break

            # DIIS Extrapolation (also updates DIIS history)

            if iteration > 0:
                F = diis.iterate(
                    state_vector=F,
                    error_vector=G,
                    )

            # Fock Matrix Diagonalization

            tmp = GPUMatrixUtility.matrix_multiply(
                mat1=X,
                mat2=F,
                transpose1=False,
                transpose2=False,
                )
            F2 = GPUMatrixUtility.matrix_multiply(
                mat1=tmp,
                mat2=X,
                transpose1=False,
                transpose2=True,
                )

            eps, U2 = GPUMatrixUtility.eigh(matrix=F2)

            C = GPUMatrixUtility.matrix_multiply(
                mat1=U2,
                mat2=X,
                transpose1=True,
                transpose2=False,
                )

            # New Occupied Orbitals and Density Matrix

            C_np = C.to_numpy()
            Cocc_np = C_np[:self.sizes['nocc'], :]
            Cocc = GPUMatrix.from_numpy(Cocc_np)

            D = GPUMatrixUtility.matrix_multiply(
                mat1=Cocc,
                mat2=Cocc,
                transpose1=True,
                transpose2=False,
                )

            self.tensors['D'] = D
            self.tensors['Cocc'] = Cocc

            self.tensors['C'] = C
            self.tensors['eps'] = eps

        # => Print Convergence <= #

        if self.print_level > 0:
            if self.is_converged:
                print('SCF Converged\n')
            else:
                print('SCF Failed\n')

        # => Print Final Energy <= #

        if self.print_level:
            print('SCF Energy = %24.16E\n' % E)

        if self.print_level > 0:
            print('=> End Solve DIIS <=\n')

    def compute_energy(self):

        if not self.is_solved:
            self.solve()

        if not self.is_converged:
            raise RuntimeError('Not converged')

        return self.scalars['Escf']

    def compute_gradient(self):

        if not self.is_solved:
            self.solve()

        if not self.is_converged:
            raise RuntimeError('Not converged')

        # Occupied orbitals / density matrix


            self.tensors['C'] = C
            self.tensors['eps'] = eps

        # => Print Convergence <= #

        if self.print_level > 0:
            if self.is_converged:
                print('SCF Converged\n')
            else:
                print('SCF Failed\n')

        # => Print Final Energy <= #

        if self.print_level:
            print('SCF Energy = %24.16E\n' % E)

        if self.print_level > 0:
            print('=> End Solve DIIS <=\n')

    def compute_energy(self):

        if not self.is_solved:
            self.solve()

        if not self.is_converged:
            raise RuntimeError('Not converged')

        return self.scalars['Escf']

    def compute_gradient(self):

        if not self.is_solved:
            self.solve()

        if not self.is_converged:
            raise RuntimeError('Not converged')

        # Occupied orbitals / density matrix

        Cocc = self.tensors['Cocc']
        D = self.tensors['D']

        # Energy-weighted density matrix

        eps_occ_np = self.tensors['eps'].to_numpy()[:self.sizes['nocc']]
        Cocc_np = Cocc.to_numpy()
        Vocc_np = np.einsum('ip,id->ipd', Cocc_np, eps_occ_np).reshape(Cocc_np.shape[0], Cocc_np.shape[1])
        W_np = np.dot(Vocc_np.T, Cocc_np)


                print('SCF Failed\n')

        # => Print Final Energy <= #

        if self.print_level:
            print('SCF Energy = %24.16E\n' % E)

        if self.print_level > 0:
            print('=> End Solve DIIS <=\n')

    def compute_energy(self):

        if not self.is_solved:
            self.solve()

        if not self.is_converged:
            raise RuntimeError('Not converged')

        return self.scalars['Escf']

    def compute_gradient(self):

        if not self.is_solved:
            self.solve()

        if not self.is_converged:
            raise RuntimeError('Not converged')

        # Occupied orbitals / density matrix

        Cocc = self.tensors['Cocc']
        D = self.tensors['D']

        # Energy-weighted density matrix

        eps_occ_np = self.tensors['eps'].to_numpy()[:self.sizes['nocc']]
        Cocc_np = Cocc.to_numpy()
        Vocc_np = np.einsum('ip,id->ipd', Cocc_np, eps_occ_np).reshape(Cocc_np.shape[0], Cocc_np.shape[1])
        W_np = np.dot(Vocc_np.T, Cocc_np)
        W = GPUMatrix.from_numpy(W_np)

        # Nuclear gradient (classical)

        Gnuc = self.compute_Enuc_gradient()
        GPUMatrixUtility.scale(
            matrix=Gnuc,
            scale=+1.0,
            )

        # Overlap gradient

        GS = self.compute_overlap_gradient(W)
        GPUMatrixUtility.scale(
            matrix=GS,
            scale=-2.0,
            )

        # Kinetic gradient

        GT = self.compute_kinetic_gradient(D)
        GPUMatrixUtility.scale(
            matrix=GT,
            scale=+2.0,
            )

        # Potential gradient

        GV = self.compute_potential_gradient(D)
        GPUMatrixUtility.scale(
            matrix=GV,
            scale=+2.0,
            )

        # United Coulomb + Exchange gradient

        GJK = self.compute_coulomb_and_exchange_gradient(
            scaleJ=+2.0,
            DJ=D,
            scaleK=-1.0,
            CoccsK=[Cocc],
            )

        # Local exchange-correlation gradient

        GVxc = self.compute_local_exchange_correlation_gradient(Cocc)
        GPUMatrixUtility.scale(
            matrix=GVxc,
            scale=+1.0,
            )

        # Nonlocal exchange-correlation gradient

        GVnlc = self.compute_nonlocal_exchange_correlation_gradient(Cocc)
        GPUMatrixUtility.scale(
            matrix=GVnlc,
            scale=+1.0,
            )

        # PCM gradient

        Gpcm = None
        if self.do_pcm:
            GPUMatrixUtility.scale(
                matrix=D,
                scale=2.0,
                )
            Gpcm = self.compute_pcm_gradient(
                q=self.pcm_q,
                D=D)
            GPUMatrixUtility.scale(
                matrix=D,
                scale=0.5,
                )

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

        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=Gnuc,
            y=G,
            )
        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=GS,
            y=G,
            )
        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=GT,
            y=G,
            )
        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=GV,
            y=G,
            )
        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=GJK,
            y=G,
            )
        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=GVxc,
            y=G,
            )
        GPUMatrixUtility.daxpy(
            alpha=1.0,
            x=GVnlc,
            y=G,
            )
        if self.do_pcm:
            GPUMatrixUtility.daxpy(
                alpha=1.0,
                x=Gpcm,
                y=G,
                )

        return G


# 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
import cuda.bindings.runtime as cuda

class CudaUtility(object):

    @staticmethod
    def cuda_malloc(size_in_bytes):
        ret, pointer = cuda.cudaMalloc(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate CUDA array:", ret)

        return pointer

    @staticmethod
    def cuda_free(pointer):

        ret = cuda.cudaFree(pointer)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to free CUDA array:", ret[0])

    @staticmethod
    def cuda_malloc_host(size_in_bytes):

        # NOTE: we are using cudaMallocHost as an expediency here.
        # Ideally, in production codes, this would be simple malloc or similar
        # as pinned memory is not required here.

        ret, pointer = cuda.cudaMallocHost(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate host array:", ret)

        return pointer

    @staticmethod
    def cuda_free_host(pointer):

        ret = cuda.cudaFreeHost(pointer)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to free host array:", ret[0])

    @staticmethod
    def cuda_memcpy_dtoh(
        *,
        host_pointer,
        device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to host
        and device memory allocations, and copy size_in_bytes bytes from the
        device memory to the host memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=host_pointer,
            src=device_pointer,
            count=size_in_bytes,
            kind=cuda.cudaMemcpyKind.cudaMemcpyDeviceToHost,
            stream=stream)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Copy from device to host failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)

    @staticmethod
    def cuda_memcpy_htod(
        *,
        device_pointer,
        host_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to host
        and device memory allocations, and copy size_in_bytes bytes from the
        host memory to the device memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=device_pointer,
            src=host_pointer,
            count=size_in_bytes,
            kind=cuda.cudaMemcpyKind.cudaMemcpyHostToDevice,
            stream=stream)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Copy from host to device failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)

    @staticmethod
    def cuda_memcpy_dtod(
        *,
        dst_device_pointer,
        src_device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to two
        device memory allocations, and copy size_in_bytes bytes from the
        src memory to the destination memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=dst_device_pointer,
            src=src_device_pointer,
            count=size_in_bytes,
            kind=cuda.cudaMemcpyKind.cudaMemcpyDeviceToDevice,
            stream=stream)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Copy from device to device failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)

    @staticmethod
    def cuda_zero_memory(
        *,
        device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        Initialize a chunk of device memory to the zero.  The stream to execute this can be optionally provided.
        """

        ret = cuda.cudaMemsetAsync(
            devPtr=device_pointer,
            value=0,
            count=size_in_bytes,
            stream=stream,
            )

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("CUDA memset failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)


# 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
import cuda.bindings.runtime as cuda

class CudaUtility(object):

    @staticmethod
    def cuda_malloc(size_in_bytes):
        ret, pointer = cuda.cudaMalloc(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate CUDA array:", ret)

        return pointer

    @staticmethod
    def cuda_free(pointer):

        ret = cuda.cudaFree(pointer)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to free CUDA array:", ret[0])

    @staticmethod
    def cuda_malloc_host(size_in_bytes):

        # NOTE: we are using cudaMallocHost as an expediency here.
        # Ideally, in production codes, this would be simple malloc or similar
        # as pinned memory is not required here.

        ret, pointer = cuda.cudaMallocHost(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate host array:", ret)


# 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
import cuda.bindings.runtime as cuda

class CudaUtility(object):

    @staticmethod
    def cuda_malloc(size_in_bytes):
        ret, pointer = cuda.cudaMalloc(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate CUDA array:", ret)

        return pointer

    @staticmethod
    def cuda_free(pointer):

        ret = cuda.cudaFree(pointer)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to free CUDA array:", ret[0])

    @staticmethod
    def cuda_malloc_host(size_in_bytes):

        # NOTE: we are using cudaMallocHost as an expediency here.
        # Ideally, in production codes, this would be simple malloc or similar
        # as pinned memory is not required here.

        ret, pointer = cuda.cudaMallocHost(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate host array:", ret)

        return pointer

    @staticmethod
    def cuda_free_host(pointer):

        ret = cuda.cudaFreeHost(pointer)


class CudaUtility(object):

    @staticmethod
    def cuda_malloc(size_in_bytes):
        ret, pointer = cuda.cudaMalloc(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate CUDA array:", ret)

        return pointer

    @staticmethod
    def cuda_free(pointer):

        ret = cuda.cudaFree(pointer)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to free CUDA array:", ret[0])

    @staticmethod
    def cuda_malloc_host(size_in_bytes):

        # NOTE: we are using cudaMallocHost as an expediency here.
        # Ideally, in production codes, this would be simple malloc or similar
        # as pinned memory is not required here.

        ret, pointer = cuda.cudaMallocHost(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate host array:", ret)

        return pointer

    @staticmethod
    def cuda_free_host(pointer):

        ret = cuda.cudaFreeHost(pointer)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to free host array:", ret[0])

    @staticmethod
    def cuda_memcpy_dtoh(
        *,
        host_pointer,
        device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to host
        and device memory allocations, and copy size_in_bytes bytes from the


        ret = cuda.cudaFree(pointer)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to free CUDA array:", ret[0])

    @staticmethod
    def cuda_malloc_host(size_in_bytes):

        # NOTE: we are using cudaMallocHost as an expediency here.
        # Ideally, in production codes, this would be simple malloc or similar
        # as pinned memory is not required here.

        ret, pointer = cuda.cudaMallocHost(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate host array:", ret)

        return pointer

    @staticmethod
    def cuda_free_host(pointer):

        ret = cuda.cudaFreeHost(pointer)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to free host array:", ret[0])

    @staticmethod
    def cuda_memcpy_dtoh(
        *,
        host_pointer,
        device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to host
        and device memory allocations, and copy size_in_bytes bytes from the
        device memory to the host memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=host_pointer,
            src=device_pointer,
            count=size_in_bytes,


        # NOTE: we are using cudaMallocHost as an expediency here.
        # Ideally, in production codes, this would be simple malloc or similar
        # as pinned memory is not required here.

        ret, pointer = cuda.cudaMallocHost(size_in_bytes)

        if ret != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to allocate host array:", ret)

        return pointer

    @staticmethod
    def cuda_free_host(pointer):

        ret = cuda.cudaFreeHost(pointer)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Unable to free host array:", ret[0])

    @staticmethod
    def cuda_memcpy_dtoh(
        *,
        host_pointer,
        device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to host
        and device memory allocations, and copy size_in_bytes bytes from the
        device memory to the host memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=host_pointer,
            src=device_pointer,
            count=size_in_bytes,
            kind=cuda.cudaMemcpyKind.cudaMemcpyDeviceToHost,
            stream=stream)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Copy from device to host failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)

    @staticmethod
    def cuda_memcpy_htod(
        *,
        device_pointer,
        host_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to host
        and device memory allocations, and copy size_in_bytes bytes from the
        host memory to the device memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=device_pointer,
            src=host_pointer,
            count=size_in_bytes,


        """
        This function should take integer representations of pointers to host
        and device memory allocations, and copy size_in_bytes bytes from the
        device memory to the host memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=host_pointer,
            src=device_pointer,
            count=size_in_bytes,
            kind=cuda.cudaMemcpyKind.cudaMemcpyDeviceToHost,
            stream=stream)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Copy from device to host failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)

    @staticmethod
    def cuda_memcpy_htod(
        *,
        device_pointer,
        host_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to host
        and device memory allocations, and copy size_in_bytes bytes from the
        host memory to the device memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=device_pointer,
            src=host_pointer,
            count=size_in_bytes,
            kind=cuda.cudaMemcpyKind.cudaMemcpyHostToDevice,
            stream=stream)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Copy from host to device failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)

    @staticmethod
    def cuda_memcpy_dtod(
        *,
        dst_device_pointer,
        src_device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to two
        device memory allocations, and copy size_in_bytes bytes from the
        src memory to the destination memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=dst_device_pointer,
            src=src_device_pointer,
            count=size_in_bytes,


        """
        This function should take integer representations of pointers to host
        and device memory allocations, and copy size_in_bytes bytes from the
        host memory to the device memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=device_pointer,
            src=host_pointer,
            count=size_in_bytes,
            kind=cuda.cudaMemcpyKind.cudaMemcpyHostToDevice,
            stream=stream)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Copy from host to device failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)

    @staticmethod
    def cuda_memcpy_dtod(
        *,
        dst_device_pointer,
        src_device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        This function should take integer representations of pointers to two
        device memory allocations, and copy size_in_bytes bytes from the
        src memory to the destination memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=dst_device_pointer,
            src=src_device_pointer,
            count=size_in_bytes,
            kind=cuda.cudaMemcpyKind.cudaMemcpyDeviceToDevice,
            stream=stream)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Copy from device to device failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)

    @staticmethod
    def cuda_zero_memory(
        *,
        device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        Initialize a chunk of device memory to the zero.  The stream to execute this can be optionally provided.
        """

        ret = cuda.cudaMemsetAsync(
            devPtr=device_pointer,
            value=0,
            count=size_in_bytes,
            stream=stream,
            )

        if ret[0] != cuda.cudaError_t.cudaSuccess:


        """
        This function should take integer representations of pointers to two
        device memory allocations, and copy size_in_bytes bytes from the
        src memory to the destination memory.  The stream to execute this can be
        optionally provided.
        """

        ret = cuda.cudaMemcpyAsync(
            dst=dst_device_pointer,
            src=src_device_pointer,
            count=size_in_bytes,
            kind=cuda.cudaMemcpyKind.cudaMemcpyDeviceToDevice,
            stream=stream)

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("Copy from device to device failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)

    @staticmethod
    def cuda_zero_memory(
        *,
        device_pointer,
        size_in_bytes,
        stream=None,
        ):
        """
        Initialize a chunk of device memory to the zero.  The stream to execute this can be optionally provided.
        """

        ret = cuda.cudaMemsetAsync(
            devPtr=device_pointer,
            value=0,
            count=size_in_bytes,
            stream=stream,
            )

        if ret[0] != cuda.cudaError_t.cudaSuccess:
            raise RuntimeError("CUDA memset failed:", ret[0])

        cuda.cudaStreamSynchronize(stream)


# 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 .ao_basis import AOBasis

from .cuest_workspace_descriptor import CuestWorkspaceDescriptor
from .cuest_workspace import CuestWorkspace

from .cuest_handle import CuestHandle
from .cuest_ao_shell import CuestAOShell

from .cuest_parameters import CuestParameters

class CuestAOBasis(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        basis : AOBasis,
        ):

        self.initialized = False

        self.handle = handle

        shells = [CuestAOShell(
            handle=handle,
            ao_shell=shell) for shell in basis.shells_unrolled]

        # Careful - "shells" owns these handles.
        # OK as we will use readonly during this constructor
        shells_raw = [_.ao_shell_handle for _ in shells]

        nshell_per_atom = [len(_) for _ in basis.shells]

        ao_basis_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            )

        self.ao_basis_handle = ce.cuestAOBasisHandle()

        # => Workspace Query <= #

        persistent_workspace_descriptor = CuestWorkspaceDescriptor()
        temporary_workspace_descriptor = CuestWorkspaceDescriptor()

        status = ce.cuestAOBasisCreateWorkspaceQuery(
            handle=handle.handle,
            numAtoms=basis.natom,
            numShellsPerAtom=nshell_per_atom,
            shells=shells_raw,
            parameters=ao_basis_parameters.parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outBasis=self.ao_basis_handle,
            )

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

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

        # => Creation <= #

        status = ce.cuestAOBasisCreate(
            handle=handle.handle,
            numAtoms=basis.natom,
            numShellsPerAtom=nshell_per_atom,
            shells=shells_raw,
            parameters=ao_basis_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outBasis=self.ao_basis_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOBasisCreate 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.cuestAOBasisDestroy(
            handle=self.ao_basis_handle,
            )

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

    @property
    def is_pure(self):
        query = ce.data_int32_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_IS_PURE,
            attributeValue=query,
            )

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

        return bool(query.value)

    @property
    def is_cart(self):
        query = ce.data_int32_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_IS_CART,
            attributeValue=query,
            )

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

        return bool(query.value)

    @property
    def is_mixed(self):
        query = ce.data_int32_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_IS_MIXED,
            attributeValue=query,
            )

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

        return bool(query.value)

    @property
    def natom(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_ATOM,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def nshell(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_SHELL,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def nao(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_AO,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def ncart(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_CART,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def nprimitive(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_PRIMITIVE,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def max_L(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_MAX_L,
            attributeValue=query,
            )

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

        return int(query.value)

    def __str__(self):
        s = ''
        s += 'CuestAOBasis:\n'
        s += '%-10s = %6d\n' % ('natom', self.natom)
        s += '%-10s = %6d\n' % ('nshell', self.nshell)
        s += '%-10s = %6d\n' % ('nao', self.nao)
        s += '%-10s = %6d\n' % ('ncart', self.ncart)
        s += '%-10s = %6d\n' % ('nprimitive', self.nprimitive)
        s += '%-10s = %6d\n' % ('max_L', self.max_L)
        s += '%-10s = %6s\n' % ('is_pure', self.is_pure)
        s += '%-10s = %6s\n' % ('is_cart', self.is_cart)
        s += '%-10s = %6s\n' % ('is_mixed', self.is_mixed)
        return s


CuestAOBasis

# 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 .ao_basis import AOBasis

from .cuest_workspace_descriptor import CuestWorkspaceDescriptor
from .cuest_workspace import CuestWorkspace

from .cuest_handle import CuestHandle
from .cuest_ao_shell import CuestAOShell

from .cuest_parameters import CuestParameters

class CuestAOBasis(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        basis : AOBasis,
        ):

        self.initialized = False

        self.handle = handle

        shells = [CuestAOShell(
            handle=handle,
            ao_shell=shell) for shell in basis.shells_unrolled]

        # Careful - "shells" owns these handles.
        # OK as we will use readonly during this constructor
        shells_raw = [_.ao_shell_handle for _ in shells]

        nshell_per_atom = [len(_) for _ in basis.shells]

        ao_basis_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            )

        self.ao_basis_handle = ce.cuestAOBasisHandle()

        # => Workspace Query <= #

        persistent_workspace_descriptor = CuestWorkspaceDescriptor()
        temporary_workspace_descriptor = CuestWorkspaceDescriptor()

        status = ce.cuestAOBasisCreateWorkspaceQuery(
            handle=handle.handle,
            numAtoms=basis.natom,
            numShellsPerAtom=nshell_per_atom,
            shells=shells_raw,
            parameters=ao_basis_parameters.parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outBasis=self.ao_basis_handle,
            )

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

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

        # => Creation <= #

        status = ce.cuestAOBasisCreate(
            handle=handle.handle,
            numAtoms=basis.natom,
            numShellsPerAtom=nshell_per_atom,
            shells=shells_raw,
            parameters=ao_basis_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outBasis=self.ao_basis_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOBasisCreate 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.cuestAOBasisDestroy(
            handle=self.ao_basis_handle,
            )

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

    @property
    def is_pure(self):
        query = ce.data_int32_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_IS_PURE,
            attributeValue=query,
            )

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

        return bool(query.value)

    @property
    def is_cart(self):
        query = ce.data_int32_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_IS_CART,
            attributeValue=query,
            )

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

        return bool(query.value)

    @property
    def is_mixed(self):
        query = ce.data_int32_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_IS_MIXED,
            attributeValue=query,
            )

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

        return bool(query.value)

    @property
    def natom(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_ATOM,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def nshell(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_SHELL,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def nao(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_AO,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def ncart(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_CART,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def nprimitive(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_PRIMITIVE,
            attributeValue=query,
            )

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

        return int(query.value)

    @property
    def max_L(self):
        query = ce.data_uint64_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_MAX_L,
            attributeValue=query,
            )

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

        return int(query.value)

    def __str__(self):
        s = ''
        s += 'CuestAOBasis:\n'
        s += '%-10s = %6d\n' % ('natom', self.natom)
        s += '%-10s = %6d\n' % ('nshell', self.nshell)
        s += '%-10s = %6d\n' % ('nao', self.nao)
        s += '%-10s = %6d\n' % ('ncart', self.ncart)
        s += '%-10s = %6d\n' % ('nprimitive', self.nprimitive)
        s += '%-10s = %6d\n' % ('max_L', self.max_L)
        s += '%-10s = %6s\n' % ('is_pure', self.is_pure)
        s += '%-10s = %6s\n' % ('is_cart', self.is_cart)
        s += '%-10s = %6s\n' % ('is_mixed', self.is_mixed)
        return s


# 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 .ao_basis import AOBasis

from .cuest_workspace_descriptor import CuestWorkspaceDescriptor
from .cuest_workspace import CuestWorkspace

from .cuest_handle import CuestHandle
from .cuest_ao_shell import CuestAOShell

from .cuest_parameters import CuestParameters

class CuestAOBasis(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        basis : AOBasis,
        ):

        self.initialized = False

        self.handle = handle

        shells = [CuestAOShell(
            handle=handle,
            ao_shell=shell) for shell in basis.shells_unrolled]

        # Careful - "shells" owns these handles.
        # OK as we will use readonly during this constructor
        shells_raw = [_.ao_shell_handle for _ in shells]

        nshell_per_atom = [len(_) for _ in basis.shells]

        ao_basis_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            )

        self.ao_basis_handle = ce.cuestAOBasisHandle()

        # => Workspace Query <= #

        persistent_workspace_descriptor = CuestWorkspaceDescriptor()
        temporary_workspace_descriptor = CuestWorkspaceDescriptor()

        status = ce.cuestAOBasisCreateWorkspaceQuery(
            handle=handle.handle,
            numAtoms=basis.natom,
            numShellsPerAtom=nshell_per_atom,
            shells=shells_raw,
            parameters=ao_basis_parameters.parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outBasis=self.ao_basis_handle,
            )

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

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

        # => Creation <= #

        status = ce.cuestAOBasisCreate(
            handle=handle.handle,
            numAtoms=basis.natom,
            numShellsPerAtom=nshell_per_atom,
            shells=shells_raw,
            parameters=ao_basis_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outBasis=self.ao_basis_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOBasisCreate 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.cuestAOBasisDestroy(
            handle=self.ao_basis_handle,
            )

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

    @property
    def is_pure(self):
        query = ce.data_int32_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,


        status = ce.cuestAOBasisCreate(
            handle=handle.handle,
            numAtoms=basis.natom,
            numShellsPerAtom=nshell_per_atom,
            shells=shells_raw,
            parameters=ao_basis_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outBasis=self.ao_basis_handle,
            )

        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestAOBasisCreate 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.cuestAOBasisDestroy(
            handle=self.ao_basis_handle,
            )

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

    @property
    def is_pure(self):
        query = ce.data_int32_t()

        status = ce.cuestQuery(
            handle=self.handle.handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=self.ao_basis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_IS_PURE,
            attributeValue=query,
            )

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

        return bool(query.value)

    @property
    def is_cart(self):