    return stream

def cuda_malloc(
    *,
    size_in_bytes,
    ):
    """
    This function should allocate device memory, and return an integer
    representation of the address of the resulting block.
    """
    ret, pointer = cuda.cudaMalloc(size_in_bytes)

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

    return pointer

def cuda_memcpy_dtoh(
    *,
    host_pointer,
    device_pointer,
    size_in_bytes,
    stream=0,
    ):
    """
    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)

def cuda_memcpy_htod(
    *,
    device_pointer,
    host_pointer,
    size_in_bytes,
    stream=0,
    ):
    """
    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)

def cuda_memcpy_htod(
    *,
    device_pointer,
    host_pointer,
    size_in_bytes,
    stream=0,
    ):
    """
    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)

def cuda_free(
    array
    ):
    """
    This function should free memory allocated on the device, passed in
    as an integer representation of its device pointer
    """

    ret = cuda.cudaFree(array)

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

def make_cublas_handle():
    """
    Helper function demonstrating how to use ctypes to access the C
    cuBLAS API to make a new cuBLAS handle.


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

def cuda_free(
    array
    ):
    """
    This function should free memory allocated on the device, passed in
    as an integer representation of its device pointer
    """

    ret = cuda.cudaFree(array)

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

def make_cublas_handle():
    """
    Helper function demonstrating how to use ctypes to access the C
    cuBLAS API to make a new cuBLAS handle.
    """
    handle = ctypes.c_void_p()

    CUBLAS_STATUS_SUCCESS = 0

    _cublas.cublasCreate_v2.restype = ctypes.c_int
    _cublas.cublasCreate_v2.argtypes = [ctypes.POINTER(ctypes.c_void_p)]

    status = _cublas.cublasCreate_v2(ctypes.byref(handle))
    if status != CUBLAS_STATUS_SUCCESS:
        raise RuntimeError(f"cublasCreate_v2 failed with status {status}")

    return handle


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

    cuda.cudaStreamSynchronize(stream)

def cuda_free(
    array
    ):
    """
    This function should free memory allocated on the device, passed in
    as an integer representation of its device pointer
    """

    ret = cuda.cudaFree(array)

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

def make_cublas_handle():
    """
    Helper function demonstrating how to use ctypes to access the C
    cuBLAS API to make a new cuBLAS handle.
    """
    handle = ctypes.c_void_p()

    CUBLAS_STATUS_SUCCESS = 0

    _cublas.cublasCreate_v2.restype = ctypes.c_int
    _cublas.cublasCreate_v2.argtypes = [ctypes.POINTER(ctypes.c_void_p)]

    status = _cublas.cublasCreate_v2(ctypes.byref(handle))
    if status != CUBLAS_STATUS_SUCCESS:
        raise RuntimeError(f"cublasCreate_v2 failed with status {status}")

    return handle

def free_cublas_handle(
    *,
    handle
    ):

    """
    Helper function to free cuBLAS resource handle.
    """

    CUBLAS_STATUS_SUCCESS = 0

    _cublas.cublasDestroy_v2.restype = ctypes.c_int
    _cublas.cublasDestroy_v2.argtypes = [ctypes.c_void_p]

    status = _cublas.cublasDestroy_v2(handle)
    if status != CUBLAS_STATUS_SUCCESS:
        raise RuntimeError(f"cublasDestroy_v2 failed with status {status}")


def make_cublas_handle():
    """
    Helper function demonstrating how to use ctypes to access the C
    cuBLAS API to make a new cuBLAS handle.
    """
    handle = ctypes.c_void_p()

    CUBLAS_STATUS_SUCCESS = 0

    _cublas.cublasCreate_v2.restype = ctypes.c_int
    _cublas.cublasCreate_v2.argtypes = [ctypes.POINTER(ctypes.c_void_p)]

    status = _cublas.cublasCreate_v2(ctypes.byref(handle))
    if status != CUBLAS_STATUS_SUCCESS:
        raise RuntimeError(f"cublasCreate_v2 failed with status {status}")

    return handle

def free_cublas_handle(
    *,
    handle
    ):

    """
    Helper function to free cuBLAS resource handle.
    """

    CUBLAS_STATUS_SUCCESS = 0

    _cublas.cublasDestroy_v2.restype = ctypes.c_int
    _cublas.cublasDestroy_v2.argtypes = [ctypes.c_void_p]

    status = _cublas.cublasDestroy_v2(handle)
    if status != CUBLAS_STATUS_SUCCESS:
        raise RuntimeError(f"cublasDestroy_v2 failed with status {status}")

def make_cusolver_handle():
    """
    Make a cuSolver handle via the CUDA C API.
    """
    handle = ctypes.c_void_p()

    CUSOLVER_STATUS_SUCCESS = 0

    _cusolver.cusolverDnCreate.restype = ctypes.c_int
    _cusolver.cusolverDnCreate.argtypes = [ctypes.POINTER(ctypes.c_void_p)]

    status = _cusolver.cusolverDnCreate(ctypes.byref(handle))
    if status != CUSOLVER_STATUS_SUCCESS:
        raise RuntimeError(f"cusolverDnCreate failed with status {status}")

    return handle


def free_cublas_handle(
    *,
    handle
    ):

    """
    Helper function to free cuBLAS resource handle.
    """

    CUBLAS_STATUS_SUCCESS = 0

    _cublas.cublasDestroy_v2.restype = ctypes.c_int
    _cublas.cublasDestroy_v2.argtypes = [ctypes.c_void_p]

    status = _cublas.cublasDestroy_v2(handle)
    if status != CUBLAS_STATUS_SUCCESS:
        raise RuntimeError(f"cublasDestroy_v2 failed with status {status}")

def make_cusolver_handle():
    """
    Make a cuSolver handle via the CUDA C API.
    """
    handle = ctypes.c_void_p()

    CUSOLVER_STATUS_SUCCESS = 0

    _cusolver.cusolverDnCreate.restype = ctypes.c_int
    _cusolver.cusolverDnCreate.argtypes = [ctypes.POINTER(ctypes.c_void_p)]

    status = _cusolver.cusolverDnCreate(ctypes.byref(handle))
    if status != CUSOLVER_STATUS_SUCCESS:
        raise RuntimeError(f"cusolverDnCreate failed with status {status}")

    return handle

def free_cusolver_handle(
    handle
    ):
    """
    Free resources associated with a cuSolver handle.
    """

    CUSOLVER_STATUS_SUCCESS = 0

    _cusolver.cusolverDnDestroy.restype = ctypes.c_int
    _cusolver.cusolverDnDestroy.argtypes = [ctypes.c_void_p]

    status = _cusolver.cusolverDnDestroy(handle)
    if status != CUSOLVER_STATUS_SUCCESS:
        raise RuntimeError(f"cusolverDnDestroy failed with status {status}")

def set_cublas_stream(


def make_cusolver_handle():
    """
    Make a cuSolver handle via the CUDA C API.
    """
    handle = ctypes.c_void_p()

    CUSOLVER_STATUS_SUCCESS = 0

    _cusolver.cusolverDnCreate.restype = ctypes.c_int
    _cusolver.cusolverDnCreate.argtypes = [ctypes.POINTER(ctypes.c_void_p)]

    status = _cusolver.cusolverDnCreate(ctypes.byref(handle))
    if status != CUSOLVER_STATUS_SUCCESS:
        raise RuntimeError(f"cusolverDnCreate failed with status {status}")

    return handle

def free_cusolver_handle(
    handle
    ):
    """
    Free resources associated with a cuSolver handle.
    """

    CUSOLVER_STATUS_SUCCESS = 0

    _cusolver.cusolverDnDestroy.restype = ctypes.c_int
    _cusolver.cusolverDnDestroy.argtypes = [ctypes.c_void_p]

    status = _cusolver.cusolverDnDestroy(handle)
    if status != CUSOLVER_STATUS_SUCCESS:
        raise RuntimeError(f"cusolverDnDestroy failed with status {status}")

def set_cublas_stream(
    *,
    handle,
    stream,
    ):
    """
    Set the active stream for a given cuBLAS handle
    """

    CUBLAS_STATUS_SUCCESS = 0

    _cublas.cublasSetStream_v2.restype = ctypes.c_int
    _cublas.cublasSetStream_v2.argtypes = [ctypes.c_void_p, ctypes.c_void_p]

    status = _cublas.cublasSetStream_v2(handle, stream)
    if status != CUBLAS_STATUS_SUCCESS:
        raise RuntimeError(f"cublasSetStream failed with status {status}")


    return handle

def free_cusolver_handle(
    handle
    ):
    """
    Free resources associated with a cuSolver handle.
    """

    CUSOLVER_STATUS_SUCCESS = 0

    _cusolver.cusolverDnDestroy.restype = ctypes.c_int
    _cusolver.cusolverDnDestroy.argtypes = [ctypes.c_void_p]

    status = _cusolver.cusolverDnDestroy(handle)
    if status != CUSOLVER_STATUS_SUCCESS:
        raise RuntimeError(f"cusolverDnDestroy failed with status {status}")

def set_cublas_stream(
    *,
    handle,
    stream,
    ):
    """
    Set the active stream for a given cuBLAS handle
    """

    CUBLAS_STATUS_SUCCESS = 0

    _cublas.cublasSetStream_v2.restype = ctypes.c_int
    _cublas.cublasSetStream_v2.argtypes = [ctypes.c_void_p, ctypes.c_void_p]

    status = _cublas.cublasSetStream_v2(handle, stream)
    if status != CUBLAS_STATUS_SUCCESS:
        raise RuntimeError(f"cublasSetStream failed with status {status}")

def set_cusolver_stream(
    *,
    handle,
    stream,
    ):
    """
    Set the stream for a given cuSolver handle.
    """

    CUSOLVER_STATUS_SUCCESS = 0

    _cusolver.cusolverDnSetStream.restype = ctypes.c_int
    _cusolver.cusolverDnSetStream.argtypes = [ctypes.c_void_p, ctypes.c_void_p]

    status = _cusolver.cusolverDnSetStream(handle, stream)
    if status != CUSOLVER_STATUS_SUCCESS:
        raise RuntimeError(f"cusolverDnSetStream failed with status {status}")


def set_cublas_stream(
    *,
    handle,
    stream,
    ):
    """
    Set the active stream for a given cuBLAS handle
    """

    CUBLAS_STATUS_SUCCESS = 0

    _cublas.cublasSetStream_v2.restype = ctypes.c_int
    _cublas.cublasSetStream_v2.argtypes = [ctypes.c_void_p, ctypes.c_void_p]

    status = _cublas.cublasSetStream_v2(handle, stream)
    if status != CUBLAS_STATUS_SUCCESS:
        raise RuntimeError(f"cublasSetStream failed with status {status}")

def set_cusolver_stream(
    *,
    handle,
    stream,
    ):
    """
    Set the stream for a given cuSolver handle.
    """

    CUSOLVER_STATUS_SUCCESS = 0

    _cusolver.cusolverDnSetStream.restype = ctypes.c_int
    _cusolver.cusolverDnSetStream.argtypes = [ctypes.c_void_p, ctypes.c_void_p]

    status = _cusolver.cusolverDnSetStream(handle, stream)
    if status != CUSOLVER_STATUS_SUCCESS:
        raise RuntimeError(f"cusolverDnSetStream failed with status {status}")

class WorkspaceDescriptor:
    """
    This helper function needs to implement the following C structure,
    and define a pointer method that returns a pointer to it.

    typedef struct {
        size_t hostBufferSizeInBytes;      ///< Required size of host workspace buffer in bytes
        size_t deviceBufferSizeInBytes;    ///< Required size of device workspace buffer in bytes
    } cuestWorkspaceDescriptor_t;

    This implementation uses Numpy, as the ctypes.data member provides a
    convenient way to access a pointer to the object
    """

    def __init__(
        self,
        *,
        host_buffer_size_in_bytes = 0,


WorkspaceDescriptor

def set_cusolver_stream(
    *,
    handle,
    stream,
    ):
    """
    Set the stream for a given cuSolver handle.
    """

    CUSOLVER_STATUS_SUCCESS = 0

    _cusolver.cusolverDnSetStream.restype = ctypes.c_int
    _cusolver.cusolverDnSetStream.argtypes = [ctypes.c_void_p, ctypes.c_void_p]

    status = _cusolver.cusolverDnSetStream(handle, stream)
    if status != CUSOLVER_STATUS_SUCCESS:
        raise RuntimeError(f"cusolverDnSetStream failed with status {status}")

class WorkspaceDescriptor:
    """
    This helper function needs to implement the following C structure,
    and define a pointer method that returns a pointer to it.

    typedef struct {
        size_t hostBufferSizeInBytes;      ///< Required size of host workspace buffer in bytes
        size_t deviceBufferSizeInBytes;    ///< Required size of device workspace buffer in bytes
    } cuestWorkspaceDescriptor_t;

    This implementation uses Numpy, as the ctypes.data member provides a
    convenient way to access a pointer to the object
    """

    def __init__(
        self,
        *,
        host_buffer_size_in_bytes = 0,
        device_buffer_size_in_bytes = 0,
        ):

        _workspace_descriptor_dtype = np.dtype([
            ("hostBufferSizeInBytes", np.uint64, ),
            ("deviceBufferSizeInBytes", np.uint64, ),
            ], align=True
            )

        self.struct = np.empty(1, dtype=_workspace_descriptor_dtype)
        self.struct['deviceBufferSizeInBytes'] = device_buffer_size_in_bytes
        self.struct['hostBufferSizeInBytes'] = host_buffer_size_in_bytes

    def __str__(
        self,
        ):

        host_size = self.struct['hostBufferSizeInBytes'].item()
        device_size = self.struct['deviceBufferSizeInBytes'].item()
        return f'host buffer size = {host_size} bytes, device buffer size = {device_size} bytes'

    @property
    def pointer(self):
        return self.struct.ctypes.data

class Workspace:
    """
    This helper function needs to implement the following C structure,
    and define a pointer method that returns a pointer to it.

    typedef struct {
        uintptr_t hostBuffer;              ///< Opaque pointer to host-side workspace buffer
        size_t hostBufferSizeInBytes;      ///< Size of host workspace in bytes
        uintptr_t deviceBuffer;            ///< Opaque pointer to device-side (GPU) workspace buffer
        size_t deviceBufferSizeInBytes;    ///< Size of device workspace in bytes
    } cuestWorkspace_t;

    This implementation uses Numpy, as the ctypes.data member provides a
    convenient way to access a pointer to the object
    """
    def __init__(
        self,
        *,
        workspaceDescriptor,


Workspace

            ("deviceBufferSizeInBytes", np.uint64, ),
            ], align=True
            )

        self.struct = np.empty(1, dtype=_workspace_descriptor_dtype)
        self.struct['deviceBufferSizeInBytes'] = device_buffer_size_in_bytes
        self.struct['hostBufferSizeInBytes'] = host_buffer_size_in_bytes

    def __str__(
        self,
        ):

        host_size = self.struct['hostBufferSizeInBytes'].item()
        device_size = self.struct['deviceBufferSizeInBytes'].item()
        return f'host buffer size = {host_size} bytes, device buffer size = {device_size} bytes'

    @property
    def pointer(self):
        return self.struct.ctypes.data

class Workspace:
    """
    This helper function needs to implement the following C structure,
    and define a pointer method that returns a pointer to it.

    typedef struct {
        uintptr_t hostBuffer;              ///< Opaque pointer to host-side workspace buffer
        size_t hostBufferSizeInBytes;      ///< Size of host workspace in bytes
        uintptr_t deviceBuffer;            ///< Opaque pointer to device-side (GPU) workspace buffer
        size_t deviceBufferSizeInBytes;    ///< Size of device workspace in bytes
    } cuestWorkspace_t;

    This implementation uses Numpy, as the ctypes.data member provides a
    convenient way to access a pointer to the object
    """
    def __init__(
        self,
        *,
        workspaceDescriptor,
        ):
        _workspace_dtype = np.dtype([
            ("hostBuffer", np.uintp, ),
            ("hostBufferSizeInBytes", np.uint64, ),
            ("deviceBuffer", np.uintp, ),
            ("deviceBufferSizeInBytes", np.uint64, ),
            ], align=True
            )

        host_buffer_size_in_bytes = workspaceDescriptor.struct['hostBufferSizeInBytes'].item()
        device_buffer_size_in_bytes = workspaceDescriptor.struct['deviceBufferSizeInBytes'].item()

        self.struct = np.empty(1, dtype=_workspace_dtype)
        self.struct['deviceBufferSizeInBytes'] = device_buffer_size_in_bytes
        self.struct['hostBufferSizeInBytes'] = host_buffer_size_in_bytes

        if device_buffer_size_in_bytes:
            ret, pointer = cuda.cudaMalloc(device_buffer_size_in_bytes)
            if ret != cuda.cudaError_t.cudaSuccess:
                raise RuntimeError("Failed to allocate workspace", ret)
            self.struct['deviceBuffer'] = pointer
        else:
            self.struct['deviceBuffer'] = 0

        if host_buffer_size_in_bytes:
            self.cpu_memory = np.zeros(host_buffer_size_in_bytes, dtype=np.int8)
            self.struct['hostBuffer'] = self.cpu_memory.ctypes.data
        else:
            self.struct['hostBuffer'] = 0
            self.cpu_memory = None

    def __del__(
        self,
        ):

        if self.struct['deviceBuffer']:
            ret = cuda.cudaFree(self.struct['deviceBuffer'].item())
            if ret[0] != cuda.cudaError_t.cudaSuccess:
                raise RuntimeError("Unable to deallocate workspace:", ret[0])

    @property
    def pointer(self):
        return self.struct.ctypes.data


    @property
    def pointer(self):
        return self.struct.ctypes.data

class Workspace:
    """
    This helper function needs to implement the following C structure,
    and define a pointer method that returns a pointer to it.

    typedef struct {
        uintptr_t hostBuffer;              ///< Opaque pointer to host-side workspace buffer
        size_t hostBufferSizeInBytes;      ///< Size of host workspace in bytes
        uintptr_t deviceBuffer;            ///< Opaque pointer to device-side (GPU) workspace buffer
        size_t deviceBufferSizeInBytes;    ///< Size of device workspace in bytes
    } cuestWorkspace_t;

    This implementation uses Numpy, as the ctypes.data member provides a
    convenient way to access a pointer to the object
    """
    def __init__(
        self,
        *,
        workspaceDescriptor,
        ):
        _workspace_dtype = np.dtype([
            ("hostBuffer", np.uintp, ),
            ("hostBufferSizeInBytes", np.uint64, ),
            ("deviceBuffer", np.uintp, ),
            ("deviceBufferSizeInBytes", np.uint64, ),
            ], align=True
            )

        host_buffer_size_in_bytes = workspaceDescriptor.struct['hostBufferSizeInBytes'].item()
        device_buffer_size_in_bytes = workspaceDescriptor.struct['deviceBufferSizeInBytes'].item()

        self.struct = np.empty(1, dtype=_workspace_dtype)
        self.struct['deviceBufferSizeInBytes'] = device_buffer_size_in_bytes
        self.struct['hostBufferSizeInBytes'] = host_buffer_size_in_bytes

        if device_buffer_size_in_bytes:
            ret, pointer = cuda.cudaMalloc(device_buffer_size_in_bytes)
            if ret != cuda.cudaError_t.cudaSuccess:
                raise RuntimeError("Failed to allocate workspace", ret)
            self.struct['deviceBuffer'] = pointer
        else:
            self.struct['deviceBuffer'] = 0

        if host_buffer_size_in_bytes:
            self.cpu_memory = np.zeros(host_buffer_size_in_bytes, dtype=np.int8)
            self.struct['hostBuffer'] = self.cpu_memory.ctypes.data
        else:
            self.struct['hostBuffer'] = 0
            self.cpu_memory = None

    def __del__(
        self,
        ):

        if self.struct['deviceBuffer']:
            ret = cuda.cudaFree(self.struct['deviceBuffer'].item())
            if ret[0] != cuda.cudaError_t.cudaSuccess:
                raise RuntimeError("Unable to deallocate workspace:", ret[0])

    @property
    def pointer(self):
        return self.struct.ctypes.data


    convenient way to access a pointer to the object
    """

    def __init__(
        self,
        *,
        host_buffer_size_in_bytes = 0,
        device_buffer_size_in_bytes = 0,
        ):

        _workspace_descriptor_dtype = np.dtype([
            ("hostBufferSizeInBytes", np.uint64, ),
            ("deviceBufferSizeInBytes", np.uint64, ),
            ], align=True
            )

        self.struct = np.empty(1, dtype=_workspace_descriptor_dtype)
        self.struct['deviceBufferSizeInBytes'] = device_buffer_size_in_bytes
        self.struct['hostBufferSizeInBytes'] = host_buffer_size_in_bytes

    def __str__(
        self,
        ):

        host_size = self.struct['hostBufferSizeInBytes'].item()
        device_size = self.struct['deviceBufferSizeInBytes'].item()
        return f'host buffer size = {host_size} bytes, device buffer size = {device_size} bytes'

    @property
    def pointer(self):
        return self.struct.ctypes.data

class Workspace:
    """
    This helper function needs to implement the following C structure,
    and define a pointer method that returns a pointer to it.

    typedef struct {
        uintptr_t hostBuffer;              ///< Opaque pointer to host-side workspace buffer
        size_t hostBufferSizeInBytes;      ///< Size of host workspace in bytes
        uintptr_t deviceBuffer;            ///< Opaque pointer to device-side (GPU) workspace buffer
        size_t deviceBufferSizeInBytes;    ///< Size of device workspace in bytes
    } cuestWorkspace_t;

    This implementation uses Numpy, as the ctypes.data member provides a
    convenient way to access a pointer to the object
    """


pointer

        else:
            self.struct['deviceBuffer'] = 0

        if host_buffer_size_in_bytes:
            self.cpu_memory = np.zeros(host_buffer_size_in_bytes, dtype=np.int8)
            self.struct['hostBuffer'] = self.cpu_memory.ctypes.data
        else:
            self.struct['hostBuffer'] = 0
            self.cpu_memory = None

    def __del__(
        self,
        ):

        if self.struct['deviceBuffer']:
            ret = cuda.cudaFree(self.struct['deviceBuffer'].item())
            if ret[0] != cuda.cudaError_t.cudaSuccess:
                raise RuntimeError("Unable to deallocate workspace:", ret[0])

    @property
    def pointer(self):
        return self.struct.ctypes.data


        self.struct = np.empty(1, dtype=_workspace_dtype)
        self.struct['deviceBufferSizeInBytes'] = device_buffer_size_in_bytes
        self.struct['hostBufferSizeInBytes'] = host_buffer_size_in_bytes

        if device_buffer_size_in_bytes:
            ret, pointer = cuda.cudaMalloc(device_buffer_size_in_bytes)
            if ret != cuda.cudaError_t.cudaSuccess:
                raise RuntimeError("Failed to allocate workspace", ret)
            self.struct['deviceBuffer'] = pointer
        else:
            self.struct['deviceBuffer'] = 0

        if host_buffer_size_in_bytes:
            self.cpu_memory = np.zeros(host_buffer_size_in_bytes, dtype=np.int8)
            self.struct['hostBuffer'] = self.cpu_memory.ctypes.data
        else:
            self.struct['hostBuffer'] = 0
            self.cpu_memory = None

    def __del__(
        self,
        ):

        if self.struct['deviceBuffer']:
            ret = cuda.cudaFree(self.struct['deviceBuffer'].item())
            if ret[0] != cuda.cudaError_t.cudaSuccess:
                raise RuntimeError("Unable to deallocate workspace:", ret[0])

    @property
    def pointer(self):
        return self.struct.ctypes.data


# 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
import numpy as np
import argparse
from pathlib import Path
import sys
import os

# Inject the directory where the example helper utilities live
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from helpers.cuda_utils import cuda_malloc, cuda_free, cuda_memcpy_dtoh, cuda_memcpy_htod, WorkspaceDescriptor, Workspace
from helpers.utilities import normalize_shell_coefficients
from helpers.parsers import simple_xyz_parser, simple_gbs_parser

def run(
    *,
    xyz_filename,
    gbs_filename,
    ):

    # => Parse User Input <= #

    symbols, xyzs, Zs = simple_xyz_parser(
        filename=xyz_filename,
        to_bohr_scale_factor=1.0 / 0.52917720859,
        )

    shellinfo = simple_gbs_parser(
        filename=gbs_filename,
        symbols=symbols,
        )
    # The screening threshold used to filter out insignificant shell pairs by their overlap
    threshold_pq = 1.0e-14

    sizeof_double = np.dtype('double').itemsize

    # => Set Up cuEST <= #

    def cuest_check(
        title,
        return_code
        ):
        " A simple helper function to call cuEST functions and check the return code. "

        if return_code != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError(f"{title} failed with code {return_code}")

    # These are user-provided functions that are responsible for allocating
    # host and device workspace arrays.  We use
    persistent_workspace_descriptor = WorkspaceDescriptor()
    temporary_workspace_descriptor = WorkspaceDescriptor()

    cuest_handle_parameters = ce.cuestHandleParameters()

    cuest_check('Create Handle Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            outParameters=cuest_handle_parameters,
            )
        )

    # Here we allow cuEST to use the default stream, cuBLAS, etc., but those
    # parameters should be set in the handle parameters before this call.
    cuest_handle = ce.cuestHandle()
    cuest_check('Create Cuest Handle',
        ce.cuestCreate(
            parameters=cuest_handle_parameters,
            handle=cuest_handle,
            )
        )

    cuest_check('Destroy Handle Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            parameters=cuest_handle_parameters,
            )
        )

    aoshell_parameters = ce.cuestAOShellParameters()
    cuest_check('Create AO Shell Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            outParameters=aoshell_parameters,
            )
        )

    # => Build Gaussian Shells From Basis Parser Info <= #

    shells = []
    n_shells_per_atom = []
    shell_count = 1
    for atom_shells in shellinfo:
        n_shells_per_atom.append(len(atom_shells))
        for shell in atom_shells:
            L = shell['L']
            exponents = shell['exponents']
            raw_coefficients = shell['coefficients']
            normalized_coefficients = normalize_shell_coefficients(
                coefficients=raw_coefficients,
                exponents=exponents,
                L=L,
                normalization=shell['normalization'],
                )
            aoshell_handle = ce.cuestAOShellHandle()
            cuest_check(f'Create AO Shell {shell_count}',
                ce.cuestAOShellCreate(
                    handle=cuest_handle,
                    isPure=shell['is_pure'],
                    L=L,
                    numPrimitive=len(exponents),
                    exponents=exponents,
                    coefficients=normalized_coefficients,
                    parameters=aoshell_parameters,
                    outShell=aoshell_handle)
                )
            shells.append(aoshell_handle)
            shell_count += 1

    cuest_check('Destroy AO Shell Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            parameters=aoshell_parameters,
            )
        )

    # => Build Basis Set From Gaussian Shells <= #

    aobasis_parameters = ce.cuestAOBasisParameters()

    cuest_check('Create AO Basis Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            outParameters=aobasis_parameters,
            )
        )

    aobasis_handle = ce.cuestAOBasisHandle()

    cuest_check('Create AOBasis Workspace Query',
        ce.cuestAOBasisCreateWorkspaceQuery(
            handle=cuest_handle,
            numAtoms=len(symbols),
            numShellsPerAtom=n_shells_per_atom,
            shells=shells,
            parameters=aobasis_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outBasis=aobasis_handle,
            )
        )

    aobasis_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    aobasis_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create AOBasis',
        ce.cuestAOBasisCreate(
            handle=cuest_handle,
            numAtoms=len(symbols),
            numShellsPerAtom=n_shells_per_atom,
            shells=shells,
            parameters=aobasis_parameters,
            persistentWorkspace=aobasis_persistent_workspace.pointer,
            temporaryWorkspace=aobasis_temporary_workspace.pointer,
            outBasis=aobasis_handle,
            )
        )

    del aobasis_temporary_workspace

    for i, shell in enumerate(shells):
        cuest_check(f'Destroy AO Shell {i+1}',
            ce.cuestAOShellDestroy(
                handle=shell,
                )
            )
    aobasis_num_ao = ce.data_uint64_t()
    cuest_check('Query AO Basis Num AO',
        ce.cuestQuery(
            handle=cuest_handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=aobasis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_AO,
            attributeValue=aobasis_num_ao,
            )
        )
    nao = aobasis_num_ao.value

    cuest_check('Destroy AO Basis Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            parameters=aobasis_parameters,
            )
        )

    # => Build Pair List <= #

    aopairlist_handle = ce.cuestAOPairListHandle()

    aopairlist_parameters = ce.cuestAOPairListParameters()

    cuest_check('Create AOPairList Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            outParameters=aopairlist_parameters,
            )
        )

    cuest_check('Create AOPairList Workspace Query',
        ce.cuestAOPairListCreateWorkspaceQuery(
            handle=cuest_handle,
            basis=aobasis_handle,
            numAtoms=len(symbols),
            xyz=xyzs,
            thresholdPQ=threshold_pq,
            parameters=aopairlist_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPairList=aopairlist_handle,
            )
        )

    aopairlist_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    aopairlist_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create AOPairList',
        ce.cuestAOPairListCreate(
            handle=cuest_handle,
            basis=aobasis_handle,
            numAtoms=len(symbols),
            xyz=xyzs,
            thresholdPQ=threshold_pq,
            parameters=aopairlist_parameters,
            persistentWorkspace=aopairlist_persistent_workspace.pointer,
            temporaryWorkspace=aopairlist_temporary_workspace.pointer,
            outPairList=aopairlist_handle,
            )
        )

    del aopairlist_temporary_workspace

    cuest_check('Destroy AOPairList Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            parameters=aopairlist_parameters,
            )
        )

    # => Build One Electron Integral Plan <= #

    oeintplan_handle = ce.cuestOEIntPlanHandle()
    oeintplan_parameters = ce.cuestOEIntPlanParameters()

    cuest_check('Create OEIntPlan Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_OEINTPLAN_PARAMETERS,
            outParameters=oeintplan_parameters,
            )
        )

    cuest_check('Create OEIntPlan Workspace Query',
        ce.cuestOEIntPlanCreateWorkspaceQuery(
            handle=cuest_handle,
            basis=aobasis_handle,
            pairList=aopairlist_handle,
            parameters=oeintplan_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPlan=oeintplan_handle,
            )
        )

    oeintplan_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    oeintplan_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create OEIntPlan',
        ce.cuestOEIntPlanCreate(
            handle=cuest_handle,
            basis=aobasis_handle,
            pairList=aopairlist_handle,
            parameters=oeintplan_parameters,
            persistentWorkspace=oeintplan_persistent_workspace.pointer,
            temporaryWorkspace=oeintplan_temporary_workspace.pointer,
            outPlan=oeintplan_handle,
            )
        )

    del oeintplan_temporary_workspace

    cuest_check('Destroy OEIntPlan Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_OEINTPLAN_PARAMETERS,
            parameters=oeintplan_parameters,
            )
        )

    # => Fake Density Matrix <= #

    densitymatrix_device_handle = ce.Pointer()
    densitymatrix_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * nao**2
        )
    densitymatrix_device_handle.value = np.intp(densitymatrix_device_pointer)
    densitymatrix_host_array = np.random.normal(size=(nao, nao)).astype(np.double)
    densitymatrix_host_array = np.ravel(densitymatrix_host_array + densitymatrix_host_array.T)
    cuda_memcpy_htod(
        device_pointer=densitymatrix_device_pointer,
        host_pointer=densitymatrix_host_array.ctypes.data,
        size_in_bytes=sizeof_double * nao**2,
        )

    # => Overlap Gradients <= #

    compute_overlap_gradient_parameters = ce.cuestOverlapDerivativeComputeParameters()
    cuest_check('Create Overlap Grad Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_OVERLAPDERIVATIVECOMPUTE_PARAMETERS,
            outParameters=compute_overlap_gradient_parameters,
            )
        )

    overlapgrad_device_handle = ce.Pointer()
    cuest_check('Compute Overlap Grad Workspace Query',
        ce.cuestOverlapDerivativeComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_overlap_gradient_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            densityMatrix=densitymatrix_device_handle,
            outGradient=overlapgrad_device_handle,
            )
        )

    overlapgrad_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    overlapgrad_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * 3 * len(symbols)
        )
    overlapgrad_device_handle.value = np.intp(overlapgrad_device_pointer)

    cuest_check('Compute Overlap Derivative',
        ce.cuestOverlapDerivativeCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_overlap_gradient_parameters,
            temporaryWorkspace=overlapgrad_temporary_workspace.pointer,
            densityMatrix=densitymatrix_device_handle,
            outGradient=overlapgrad_device_handle,
            )
        )

    del overlapgrad_temporary_workspace
    cuest_check('Destroy Overlap Grad Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_OVERLAPDERIVATIVECOMPUTE_PARAMETERS,
            parameters=compute_overlap_gradient_parameters,
            )
        )

    overlapgrad_host_array = np.empty(
        (3 * len(symbols),),
        dtype=np.double
        )

    overlapgrad_host_handle = ce.Pointer(overlapgrad_host_array.ctypes.data)
    cuda_memcpy_dtoh(
        host_pointer=overlapgrad_host_array.ctypes.data,
        device_pointer=overlapgrad_device_pointer,
        size_in_bytes=sizeof_double * 3 * len(symbols),
        )

    cuda_free(
        array=overlapgrad_device_pointer
        )

    # => Kinetic Gradients <= #

    compute_kinetic_gradient_parameters = ce.cuestKineticDerivativeComputeParameters()
    cuest_check('Create Kinetic Grad Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_KINETICDERIVATIVECOMPUTE_PARAMETERS,
            outParameters=compute_kinetic_gradient_parameters,
            )
        )

    kineticgrad_device_handle = ce.Pointer()
    cuest_check('Compute Kinetic Grad Workspace Query',
        ce.cuestKineticDerivativeComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_kinetic_gradient_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            densityMatrix=densitymatrix_device_handle,
            outGradient=kineticgrad_device_handle,
            )
        )

    kineticgrad_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    kineticgrad_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * 3 * len(symbols)
        )
    kineticgrad_device_handle.value = np.intp(kineticgrad_device_pointer)

    cuest_check('Compute Kinetic Derivative',
        ce.cuestKineticDerivativeCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_kinetic_gradient_parameters,
            temporaryWorkspace=kineticgrad_temporary_workspace.pointer,
            densityMatrix=densitymatrix_device_handle,
            outGradient=kineticgrad_device_handle,
            )
        )

    del kineticgrad_temporary_workspace
    cuest_check('Destroy Kinetic Grad Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_KINETICDERIVATIVECOMPUTE_PARAMETERS,
            parameters=compute_kinetic_gradient_parameters,
            )
        )

    kineticgrad_host_array = np.empty(
        (3 * len(symbols),),
        dtype=np.double
        )

    kineticgrad_host_handle = ce.Pointer(kineticgrad_host_array.ctypes.data)
    cuda_memcpy_dtoh(
        host_pointer=kineticgrad_host_array.ctypes.data,
        device_pointer=kineticgrad_device_pointer,
        size_in_bytes=sizeof_double * 3 * len(symbols),
        )

    cuda_free(
        array=kineticgrad_device_pointer
        )

    # => Cleanup <= #

    cuda_free(
        array=densitymatrix_device_pointer
        )

    cuest_check('Destroy OEIntPlan',
        ce.cuestOEIntPlanDestroy(
            handle=oeintplan_handle,
            )
        )

    cuest_check('Destroy AOPairList',
        ce.cuestAOPairListDestroy(
            handle=aopairlist_handle,
            )
        )

    cuest_check('Destroy AOBasis',
        ce.cuestAOBasisDestroy(
            handle=aobasis_handle,
            )
        )

    del oeintplan_persistent_workspace
    del aopairlist_persistent_workspace
    del aobasis_persistent_workspace

    cuest_check('Destroy Cuest Handle',
        ce.cuestDestroy(
            handle=cuest_handle,
            )
        )

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Run using XYZ and GBS files."
        )
    parser.add_argument(
        "xyz_filename",
        type=str,
        help="The file with molecular coordinates (Angstrom) in basic xyz format."
    )
    parser.add_argument(
        "gbs_filename",
        type=str,
        help="The basis set file in G94/Psi4 GBS format."
    )

    args = parser.parse_args()

    run(
        xyz_filename=args.xyz_filename,
        gbs_filename=args.gbs_filename,
        )


run

#
# 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
import numpy as np
import argparse
from pathlib import Path
import sys
import os

# Inject the directory where the example helper utilities live
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from helpers.cuda_utils import cuda_malloc, cuda_free, cuda_memcpy_dtoh, cuda_memcpy_htod, WorkspaceDescriptor, Workspace
from helpers.utilities import normalize_shell_coefficients
from helpers.parsers import simple_xyz_parser, simple_gbs_parser

def run(
    *,
    xyz_filename,
    gbs_filename,
    ):

    # => Parse User Input <= #

    symbols, xyzs, Zs = simple_xyz_parser(
        filename=xyz_filename,
        to_bohr_scale_factor=1.0 / 0.52917720859,
        )

    shellinfo = simple_gbs_parser(
        filename=gbs_filename,
        symbols=symbols,
        )
    # The screening threshold used to filter out insignificant shell pairs by their overlap
    threshold_pq = 1.0e-14

    sizeof_double = np.dtype('double').itemsize

    # => Set Up cuEST <= #

    def cuest_check(
        title,
        return_code
        ):
        " A simple helper function to call cuEST functions and check the return code. "

        if return_code != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError(f"{title} failed with code {return_code}")

    # These are user-provided functions that are responsible for allocating
    # host and device workspace arrays.  We use
    persistent_workspace_descriptor = WorkspaceDescriptor()
    temporary_workspace_descriptor = WorkspaceDescriptor()

    cuest_handle_parameters = ce.cuestHandleParameters()

    cuest_check('Create Handle Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            outParameters=cuest_handle_parameters,
            )
        )

    # Here we allow cuEST to use the default stream, cuBLAS, etc., but those
    # parameters should be set in the handle parameters before this call.
    cuest_handle = ce.cuestHandle()
    cuest_check('Create Cuest Handle',
        ce.cuestCreate(
            parameters=cuest_handle_parameters,
            handle=cuest_handle,
            )
        )

    cuest_check('Destroy Handle Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            parameters=cuest_handle_parameters,
            )
        )

    aoshell_parameters = ce.cuestAOShellParameters()
    cuest_check('Create AO Shell Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            outParameters=aoshell_parameters,
            )
        )

    # => Build Gaussian Shells From Basis Parser Info <= #

    shells = []
    n_shells_per_atom = []
    shell_count = 1
    for atom_shells in shellinfo:
        n_shells_per_atom.append(len(atom_shells))
        for shell in atom_shells:
            L = shell['L']
            exponents = shell['exponents']
            raw_coefficients = shell['coefficients']
            normalized_coefficients = normalize_shell_coefficients(
                coefficients=raw_coefficients,
                exponents=exponents,
                L=L,
                normalization=shell['normalization'],
                )
            aoshell_handle = ce.cuestAOShellHandle()
            cuest_check(f'Create AO Shell {shell_count}',
                ce.cuestAOShellCreate(
                    handle=cuest_handle,
                    isPure=shell['is_pure'],
                    L=L,
                    numPrimitive=len(exponents),
                    exponents=exponents,
                    coefficients=normalized_coefficients,
                    parameters=aoshell_parameters,
                    outShell=aoshell_handle)
                )
            shells.append(aoshell_handle)
            shell_count += 1

    cuest_check('Destroy AO Shell Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            parameters=aoshell_parameters,
            )
        )

    # => Build Basis Set From Gaussian Shells <= #

    aobasis_parameters = ce.cuestAOBasisParameters()

    cuest_check('Create AO Basis Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            outParameters=aobasis_parameters,
            )
        )

    aobasis_handle = ce.cuestAOBasisHandle()

    cuest_check('Create AOBasis Workspace Query',
        ce.cuestAOBasisCreateWorkspaceQuery(
            handle=cuest_handle,
            numAtoms=len(symbols),
            numShellsPerAtom=n_shells_per_atom,
            shells=shells,
            parameters=aobasis_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outBasis=aobasis_handle,
            )
        )

    aobasis_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    aobasis_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create AOBasis',
        ce.cuestAOBasisCreate(
            handle=cuest_handle,
            numAtoms=len(symbols),
            numShellsPerAtom=n_shells_per_atom,
            shells=shells,
            parameters=aobasis_parameters,
            persistentWorkspace=aobasis_persistent_workspace.pointer,
            temporaryWorkspace=aobasis_temporary_workspace.pointer,
            outBasis=aobasis_handle,
            )
        )

    del aobasis_temporary_workspace

    for i, shell in enumerate(shells):
        cuest_check(f'Destroy AO Shell {i+1}',
            ce.cuestAOShellDestroy(
                handle=shell,
                )
            )
    aobasis_num_ao = ce.data_uint64_t()
    cuest_check('Query AO Basis Num AO',
        ce.cuestQuery(
            handle=cuest_handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=aobasis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_AO,
            attributeValue=aobasis_num_ao,
            )
        )
    nao = aobasis_num_ao.value

    cuest_check('Destroy AO Basis Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            parameters=aobasis_parameters,
            )
        )

    # => Build Pair List <= #

    aopairlist_handle = ce.cuestAOPairListHandle()

    aopairlist_parameters = ce.cuestAOPairListParameters()

    cuest_check('Create AOPairList Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            outParameters=aopairlist_parameters,
            )
        )

    cuest_check('Create AOPairList Workspace Query',
        ce.cuestAOPairListCreateWorkspaceQuery(
            handle=cuest_handle,
            basis=aobasis_handle,
            numAtoms=len(symbols),
            xyz=xyzs,
            thresholdPQ=threshold_pq,
            parameters=aopairlist_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPairList=aopairlist_handle,
            )
        )

    aopairlist_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    aopairlist_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create AOPairList',
        ce.cuestAOPairListCreate(
            handle=cuest_handle,
            basis=aobasis_handle,
            numAtoms=len(symbols),
            xyz=xyzs,
            thresholdPQ=threshold_pq,
            parameters=aopairlist_parameters,
            persistentWorkspace=aopairlist_persistent_workspace.pointer,
            temporaryWorkspace=aopairlist_temporary_workspace.pointer,
            outPairList=aopairlist_handle,
            )
        )

    del aopairlist_temporary_workspace

    cuest_check('Destroy AOPairList Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            parameters=aopairlist_parameters,
            )
        )

    # => Build One Electron Integral Plan <= #

    oeintplan_handle = ce.cuestOEIntPlanHandle()
    oeintplan_parameters = ce.cuestOEIntPlanParameters()

    cuest_check('Create OEIntPlan Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_OEINTPLAN_PARAMETERS,
            outParameters=oeintplan_parameters,
            )
        )

    cuest_check('Create OEIntPlan Workspace Query',
        ce.cuestOEIntPlanCreateWorkspaceQuery(
            handle=cuest_handle,
            basis=aobasis_handle,
            pairList=aopairlist_handle,
            parameters=oeintplan_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPlan=oeintplan_handle,
            )
        )

    oeintplan_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    oeintplan_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create OEIntPlan',
        ce.cuestOEIntPlanCreate(
            handle=cuest_handle,
            basis=aobasis_handle,
            pairList=aopairlist_handle,
            parameters=oeintplan_parameters,
            persistentWorkspace=oeintplan_persistent_workspace.pointer,
            temporaryWorkspace=oeintplan_temporary_workspace.pointer,
            outPlan=oeintplan_handle,
            )
        )

    del oeintplan_temporary_workspace

    cuest_check('Destroy OEIntPlan Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_OEINTPLAN_PARAMETERS,
            parameters=oeintplan_parameters,
            )
        )

    # => Fake Density Matrix <= #

    densitymatrix_device_handle = ce.Pointer()
    densitymatrix_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * nao**2
        )
    densitymatrix_device_handle.value = np.intp(densitymatrix_device_pointer)
    densitymatrix_host_array = np.random.normal(size=(nao, nao)).astype(np.double)
    densitymatrix_host_array = np.ravel(densitymatrix_host_array + densitymatrix_host_array.T)
    cuda_memcpy_htod(
        device_pointer=densitymatrix_device_pointer,
        host_pointer=densitymatrix_host_array.ctypes.data,
        size_in_bytes=sizeof_double * nao**2,
        )

    # => Overlap Gradients <= #

    compute_overlap_gradient_parameters = ce.cuestOverlapDerivativeComputeParameters()
    cuest_check('Create Overlap Grad Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_OVERLAPDERIVATIVECOMPUTE_PARAMETERS,
            outParameters=compute_overlap_gradient_parameters,
            )
        )

    overlapgrad_device_handle = ce.Pointer()
    cuest_check('Compute Overlap Grad Workspace Query',
        ce.cuestOverlapDerivativeComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_overlap_gradient_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            densityMatrix=densitymatrix_device_handle,
            outGradient=overlapgrad_device_handle,
            )
        )

    overlapgrad_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    overlapgrad_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * 3 * len(symbols)
        )
    overlapgrad_device_handle.value = np.intp(overlapgrad_device_pointer)

    cuest_check('Compute Overlap Derivative',
        ce.cuestOverlapDerivativeCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_overlap_gradient_parameters,
            temporaryWorkspace=overlapgrad_temporary_workspace.pointer,
            densityMatrix=densitymatrix_device_handle,
            outGradient=overlapgrad_device_handle,
            )
        )

    del overlapgrad_temporary_workspace
    cuest_check('Destroy Overlap Grad Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_OVERLAPDERIVATIVECOMPUTE_PARAMETERS,
            parameters=compute_overlap_gradient_parameters,
            )
        )

    overlapgrad_host_array = np.empty(
        (3 * len(symbols),),
        dtype=np.double
        )

    overlapgrad_host_handle = ce.Pointer(overlapgrad_host_array.ctypes.data)
    cuda_memcpy_dtoh(
        host_pointer=overlapgrad_host_array.ctypes.data,
        device_pointer=overlapgrad_device_pointer,
        size_in_bytes=sizeof_double * 3 * len(symbols),
        )

    cuda_free(
        array=overlapgrad_device_pointer
        )

    # => Kinetic Gradients <= #

    compute_kinetic_gradient_parameters = ce.cuestKineticDerivativeComputeParameters()
    cuest_check('Create Kinetic Grad Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_KINETICDERIVATIVECOMPUTE_PARAMETERS,
            outParameters=compute_kinetic_gradient_parameters,
            )
        )

    kineticgrad_device_handle = ce.Pointer()
    cuest_check('Compute Kinetic Grad Workspace Query',
        ce.cuestKineticDerivativeComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_kinetic_gradient_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            densityMatrix=densitymatrix_device_handle,
            outGradient=kineticgrad_device_handle,
            )
        )

    kineticgrad_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    kineticgrad_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * 3 * len(symbols)
        )
    kineticgrad_device_handle.value = np.intp(kineticgrad_device_pointer)

    cuest_check('Compute Kinetic Derivative',
        ce.cuestKineticDerivativeCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_kinetic_gradient_parameters,
            temporaryWorkspace=kineticgrad_temporary_workspace.pointer,
            densityMatrix=densitymatrix_device_handle,
            outGradient=kineticgrad_device_handle,
            )
        )

    del kineticgrad_temporary_workspace
    cuest_check('Destroy Kinetic Grad Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_KINETICDERIVATIVECOMPUTE_PARAMETERS,
            parameters=compute_kinetic_gradient_parameters,
            )
        )

    kineticgrad_host_array = np.empty(
        (3 * len(symbols),),
        dtype=np.double
        )

    kineticgrad_host_handle = ce.Pointer(kineticgrad_host_array.ctypes.data)
    cuda_memcpy_dtoh(
        host_pointer=kineticgrad_host_array.ctypes.data,
        device_pointer=kineticgrad_device_pointer,
        size_in_bytes=sizeof_double * 3 * len(symbols),
        )

    cuda_free(
        array=kineticgrad_device_pointer
        )

    # => Cleanup <= #

    cuda_free(
        array=densitymatrix_device_pointer
        )

    cuest_check('Destroy OEIntPlan',
        ce.cuestOEIntPlanDestroy(
            handle=oeintplan_handle,
            )
        )

    cuest_check('Destroy AOPairList',
        ce.cuestAOPairListDestroy(
            handle=aopairlist_handle,
            )
        )

    cuest_check('Destroy AOBasis',
        ce.cuestAOBasisDestroy(
            handle=aobasis_handle,
            )
        )

    del oeintplan_persistent_workspace
    del aopairlist_persistent_workspace
    del aobasis_persistent_workspace

    cuest_check('Destroy Cuest Handle',
        ce.cuestDestroy(
            handle=cuest_handle,
            )
        )

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Run using XYZ and GBS files."
        )
    parser.add_argument(
        "xyz_filename",
        type=str,
        help="The file with molecular coordinates (Angstrom) in basic xyz format."
    )
    parser.add_argument(
        "gbs_filename",
        type=str,
        help="The basis set file in G94/Psi4 GBS format."
    )

    args = parser.parse_args()

    run(
        xyz_filename=args.xyz_filename,


    ):

    # => Parse User Input <= #

    symbols, xyzs, Zs = simple_xyz_parser(
        filename=xyz_filename,
        to_bohr_scale_factor=1.0 / 0.52917720859,
        )

    shellinfo = simple_gbs_parser(
        filename=gbs_filename,
        symbols=symbols,
        )
    # The screening threshold used to filter out insignificant shell pairs by their overlap
    threshold_pq = 1.0e-14

    sizeof_double = np.dtype('double').itemsize

    # => Set Up cuEST <= #

    def cuest_check(
        title,
        return_code
        ):
        " A simple helper function to call cuEST functions and check the return code. "

        if return_code != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError(f"{title} failed with code {return_code}")

    # These are user-provided functions that are responsible for allocating
    # host and device workspace arrays.  We use
    persistent_workspace_descriptor = WorkspaceDescriptor()
    temporary_workspace_descriptor = WorkspaceDescriptor()

    cuest_handle_parameters = ce.cuestHandleParameters()

    cuest_check('Create Handle Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            outParameters=cuest_handle_parameters,
            )
        )

    # Here we allow cuEST to use the default stream, cuBLAS, etc., but those
    # parameters should be set in the handle parameters before this call.
    cuest_handle = ce.cuestHandle()
    cuest_check('Create Cuest Handle',
        ce.cuestCreate(


# 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
import numpy as np
import argparse
from pathlib import Path
import sys
import os

# Inject the directory where the example helper utilities live
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from helpers.cuda_utils import cuda_malloc, cuda_free, cuda_memcpy_dtoh, cuda_memcpy_htod, WorkspaceDescriptor, Workspace
from helpers.utilities import normalize_shell_coefficients
from helpers.parsers import simple_xyz_parser, simple_gbs_parser

def run(
    *,
    xyz_filename,
    gbs_filename,
    ):

    # => Parse User Input <= #

    symbols, xyzs, Zs = simple_xyz_parser(
        filename=xyz_filename,
        to_bohr_scale_factor=1.0 / 0.52917720859,
        )

    shellinfo = simple_gbs_parser(
        filename=gbs_filename,
        symbols=symbols,
        )
    # The screening threshold used to filter out insignificant shell pairs by their overlap
    threshold_pq = 1.0e-14

    sizeof_double = np.dtype('double').itemsize

    # => Set Up cuEST <= #

    def cuest_check(
        title,
        return_code
        ):
        " A simple helper function to call cuEST functions and check the return code. "

        if return_code != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError(f"{title} failed with code {return_code}")

    # These are user-provided functions that are responsible for allocating
    # host and device workspace arrays.  We use
    persistent_workspace_descriptor = WorkspaceDescriptor()
    temporary_workspace_descriptor = WorkspaceDescriptor()

    cuest_handle_parameters = ce.cuestHandleParameters()

    cuest_check('Create Handle Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            outParameters=cuest_handle_parameters,
            )
        )

    # Here we allow cuEST to use the default stream, cuBLAS, etc., but those
    # parameters should be set in the handle parameters before this call.
    cuest_handle = ce.cuestHandle()
    cuest_check('Create Cuest Handle',
        ce.cuestCreate(
            parameters=cuest_handle_parameters,
            handle=cuest_handle,
            )
        )

    cuest_check('Destroy Handle Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            parameters=cuest_handle_parameters,
            )
        )

    aoshell_parameters = ce.cuestAOShellParameters()
    cuest_check('Create AO Shell Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            outParameters=aoshell_parameters,
            )
        )

    # => Build Gaussian Shells From Basis Parser Info <= #

    shells = []
    n_shells_per_atom = []
    shell_count = 1
    for atom_shells in shellinfo:
        n_shells_per_atom.append(len(atom_shells))
        for shell in atom_shells:
            L = shell['L']
            exponents = shell['exponents']
            raw_coefficients = shell['coefficients']
            normalized_coefficients = normalize_shell_coefficients(
                coefficients=raw_coefficients,
                exponents=exponents,
                L=L,
                normalization=shell['normalization'],
                )
            aoshell_handle = ce.cuestAOShellHandle()
            cuest_check(f'Create AO Shell {shell_count}',
                ce.cuestAOShellCreate(
                    handle=cuest_handle,
                    isPure=shell['is_pure'],
                    L=L,
                    numPrimitive=len(exponents),
                    exponents=exponents,
                    coefficients=normalized_coefficients,
                    parameters=aoshell_parameters,
                    outShell=aoshell_handle)
                )
            shells.append(aoshell_handle)
            shell_count += 1

    cuest_check('Destroy AO Shell Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            parameters=aoshell_parameters,
            )
        )

    # => Build Basis Set From Gaussian Shells <= #

    aobasis_parameters = ce.cuestAOBasisParameters()

    cuest_check('Create AO Basis Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            outParameters=aobasis_parameters,
            )
        )

    aobasis_handle = ce.cuestAOBasisHandle()

    cuest_check('Create AOBasis Workspace Query',
        ce.cuestAOBasisCreateWorkspaceQuery(
            handle=cuest_handle,
            numAtoms=len(symbols),
            numShellsPerAtom=n_shells_per_atom,
            shells=shells,
            parameters=aobasis_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outBasis=aobasis_handle,
            )
        )

    aobasis_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    aobasis_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create AOBasis',
        ce.cuestAOBasisCreate(
            handle=cuest_handle,
            numAtoms=len(symbols),
            numShellsPerAtom=n_shells_per_atom,
            shells=shells,
            parameters=aobasis_parameters,
            persistentWorkspace=aobasis_persistent_workspace.pointer,
            temporaryWorkspace=aobasis_temporary_workspace.pointer,
            outBasis=aobasis_handle,
            )
        )

    del aobasis_temporary_workspace

    for i, shell in enumerate(shells):
        cuest_check(f'Destroy AO Shell {i+1}',
            ce.cuestAOShellDestroy(
                handle=shell,
                )
            )
    aobasis_num_ao = ce.data_uint64_t()
    cuest_check('Query AO Basis Num AO',
        ce.cuestQuery(
            handle=cuest_handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=aobasis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_AO,
            attributeValue=aobasis_num_ao,
            )
        )
    nao = aobasis_num_ao.value

    cuest_check('Destroy AO Basis Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            parameters=aobasis_parameters,
            )
        )

    # => Build Pair List <= #

    aopairlist_handle = ce.cuestAOPairListHandle()

    aopairlist_parameters = ce.cuestAOPairListParameters()

    cuest_check('Create AOPairList Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            outParameters=aopairlist_parameters,
            )
        )

    cuest_check('Create AOPairList Workspace Query',
        ce.cuestAOPairListCreateWorkspaceQuery(
            handle=cuest_handle,
            basis=aobasis_handle,
            numAtoms=len(symbols),
            xyz=xyzs,
            thresholdPQ=threshold_pq,
            parameters=aopairlist_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPairList=aopairlist_handle,
            )
        )

    aopairlist_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    aopairlist_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create AOPairList',
        ce.cuestAOPairListCreate(
            handle=cuest_handle,
            basis=aobasis_handle,
            numAtoms=len(symbols),
            xyz=xyzs,
            thresholdPQ=threshold_pq,
            parameters=aopairlist_parameters,
            persistentWorkspace=aopairlist_persistent_workspace.pointer,
            temporaryWorkspace=aopairlist_temporary_workspace.pointer,
            outPairList=aopairlist_handle,
            )
        )

    del aopairlist_temporary_workspace

    cuest_check('Destroy AOPairList Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            parameters=aopairlist_parameters,
            )
        )

    # => Build One Electron Integral Plan <= #

    oeintplan_handle = ce.cuestOEIntPlanHandle()
    oeintplan_parameters = ce.cuestOEIntPlanParameters()

    cuest_check('Create OEIntPlan Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_OEINTPLAN_PARAMETERS,
            outParameters=oeintplan_parameters,
            )
        )

    cuest_check('Create OEIntPlan Workspace Query',
        ce.cuestOEIntPlanCreateWorkspaceQuery(
            handle=cuest_handle,
            basis=aobasis_handle,
            pairList=aopairlist_handle,
            parameters=oeintplan_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPlan=oeintplan_handle,
            )
        )

    oeintplan_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    oeintplan_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create OEIntPlan',
        ce.cuestOEIntPlanCreate(
            handle=cuest_handle,
            basis=aobasis_handle,
            pairList=aopairlist_handle,
            parameters=oeintplan_parameters,
            persistentWorkspace=oeintplan_persistent_workspace.pointer,
            temporaryWorkspace=oeintplan_temporary_workspace.pointer,
            outPlan=oeintplan_handle,
            )
        )

    del oeintplan_temporary_workspace

    cuest_check('Destroy OEIntPlan Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_OEINTPLAN_PARAMETERS,
            parameters=oeintplan_parameters,
            )
        )

    # => Overlap Integrals <= #

    overlapint_device_handle = ce.Pointer()
    compute_overlap_parameters = ce.cuestOverlapComputeParameters()
    cuest_check('Create Overlap Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_OVERLAPCOMPUTE_PARAMETERS,
            outParameters=compute_overlap_parameters,
            )
        )

    # Find Memory Requirements
    cuest_check('Compute Overlap Ints Workspace Query',
        ce.cuestOverlapComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_overlap_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outSMatrix=overlapint_device_handle,
            )
        )

    overlapint_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    overlapint_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * nao**2
        )
    overlapint_device_handle.value = np.intp(overlapint_device_pointer)

    # Compute Integrals
    cuest_check('Compute Overlap Ints',
        ce.cuestOverlapCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_overlap_parameters,
            temporaryWorkspace=overlapint_temporary_workspace.pointer,
            outSMatrix=overlapint_device_handle,
            )
        )

    del overlapint_temporary_workspace

    cuest_check('Destroy Overlap Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_OVERLAPCOMPUTE_PARAMETERS,
            parameters=compute_overlap_parameters,
            )
        )

    overlapint_host_array = np.empty(
        (nao * nao),
        dtype=np.double
        )

    cuda_memcpy_dtoh(
        host_pointer=overlapint_host_array.ctypes.data,
        device_pointer=overlapint_device_pointer,
        size_in_bytes=sizeof_double * nao**2,
        )

    cuda_free(
        array=overlapint_device_pointer
        )

    # => Kinetic Integrals <= #

    kineticint_device_handle = ce.Pointer()
    compute_kinetic_parameters = ce.cuestKineticComputeParameters()
    cuest_check('Create Kinetic Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_KINETICCOMPUTE_PARAMETERS,
            outParameters=compute_kinetic_parameters,
            )
        )

    # Find Memory Requirements
    cuest_check('Compute Kinetic Ints Workspace Query',
        ce.cuestKineticComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_kinetic_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outTMatrix=kineticint_device_handle,
            )
        )

    kineticint_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    kineticint_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * nao**2
        )
    kineticint_device_handle.value = np.intp(kineticint_device_pointer)

    # Compute Integrals
    cuest_check('Compute Kinetic Ints',
        ce.cuestKineticCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_kinetic_parameters,
            temporaryWorkspace=kineticint_temporary_workspace.pointer,
            outTMatrix=kineticint_device_handle,
            )
        )

    del kineticint_temporary_workspace

    cuest_check('Destroy Kinetic Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_KINETICCOMPUTE_PARAMETERS,
            parameters=compute_kinetic_parameters,
            )
        )

    kineticint_host_array = np.empty(
        (nao * nao),
        dtype=np.double
        )

    cuda_memcpy_dtoh(
        host_pointer=kineticint_host_array.ctypes.data,
        device_pointer=kineticint_device_pointer,
        size_in_bytes=sizeof_double * nao**2,
        )

    cuda_free(
        array=kineticint_device_pointer
        )

    # => Potential Integrals <= #

    potentialint_device_handle = ce.Pointer()

    # Move the position and (negated) charges of the atoms to the GPU
    xyzs_device_handle = ce.Pointer()
    xyzs = np.array(xyzs)
    xyzs_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * len(xyzs)
        )
    xyzs_device_handle.value = np.intp(xyzs_device_pointer)
    cuda_memcpy_htod(
        device_pointer=xyzs_device_pointer,
        host_pointer=xyzs.ctypes.data,
        size_in_bytes=sizeof_double * len(xyzs),
        )

    # The integral compute in cuEST are electron repulsion integrals but we're intersted
    # in a nucleus in the ket here, so the Z values have been scaled by -1 in the parser
    Zs_device_handle = ce.Pointer()
    Zs = np.array(Zs)
    Zs_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * len(Zs)
        )
    Zs_device_handle.value = np.intp(Zs_device_pointer)
    cuda_memcpy_htod(
        device_pointer=Zs_device_pointer,
        host_pointer=Zs.ctypes.data,
        size_in_bytes=sizeof_double * len(Zs),
        )

    potential_compute_parameters = ce.cuestPotentialComputeParameters()
    cuest_check('Create Potential Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_POTENTIALCOMPUTE_PARAMETERS,
            outParameters=potential_compute_parameters,
            )
        )

    # Find Memory Requirements
    cuest_check('Compute potential Ints Workspace Query',
        ce.cuestPotentialComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=potential_compute_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            numCharges=len(symbols),
            xyz=xyzs_device_handle,
            q=Zs_device_handle,
            outVMatrix=potentialint_device_handle,
            )
        )

    potentialint_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    potentialint_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * nao**2
        )
    potentialint_device_handle.value = np.intp(potentialint_device_pointer)

    # Compute Integrals
    cuest_check('Compute potential Ints',
        ce.cuestPotentialCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=potential_compute_parameters,
            temporaryWorkspace=potentialint_temporary_workspace.pointer,
            numCharges=len(symbols),
            xyz=xyzs_device_handle,
            q=Zs_device_handle,
            outVMatrix=potentialint_device_handle,
            )
        )
    cuda_free(
        array=xyzs_device_pointer,
        )
    cuda_free(
        array=Zs_device_pointer,
        )

    del potentialint_temporary_workspace

    cuest_check('Destroy Potential Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_POTENTIALCOMPUTE_PARAMETERS,
            parameters=potential_compute_parameters,
            )
        )

    potentialint_host_array = np.empty(
        (nao * nao),
        dtype=np.double
        )

    cuda_memcpy_dtoh(
        host_pointer=potentialint_host_array.ctypes.data,
        device_pointer=potentialint_device_pointer,
        size_in_bytes=sizeof_double * nao**2,
        )

    cuda_free(
        array=potentialint_device_pointer
        )

    # => Cleanup <= #

    cuest_check('Destroy OEIntPlan',
        ce.cuestOEIntPlanDestroy(
            handle=oeintplan_handle,
            )
        )

    cuest_check('Destroy AOPairList',
        ce.cuestAOPairListDestroy(
            handle=aopairlist_handle,
            )
        )

    cuest_check('Destroy AOBasis',
        ce.cuestAOBasisDestroy(
            handle=aobasis_handle,
            )
        )

    del oeintplan_persistent_workspace
    del aopairlist_persistent_workspace
    del aobasis_persistent_workspace

    cuest_check('Destroy Cuest Handle',
        ce.cuestDestroy(
            handle=cuest_handle,
            )
        )

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Run using XYZ and GBS files."
        )
    parser.add_argument(
        "xyz_filename",
        type=str,
        help="The file with molecular coordinates (Angstrom) in basic xyz format."
    )
    parser.add_argument(
        "gbs_filename",
        type=str,
        help="The basis set file in G94/Psi4 GBS format."
    )

    args = parser.parse_args()

    run(
        xyz_filename=args.xyz_filename,
        gbs_filename=args.gbs_filename,
        )


#
# 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
import numpy as np
import argparse
from pathlib import Path
import sys
import os

# Inject the directory where the example helper utilities live
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
from helpers.cuda_utils import cuda_malloc, cuda_free, cuda_memcpy_dtoh, cuda_memcpy_htod, WorkspaceDescriptor, Workspace
from helpers.utilities import normalize_shell_coefficients
from helpers.parsers import simple_xyz_parser, simple_gbs_parser

def run(
    *,
    xyz_filename,
    gbs_filename,
    ):

    # => Parse User Input <= #

    symbols, xyzs, Zs = simple_xyz_parser(
        filename=xyz_filename,
        to_bohr_scale_factor=1.0 / 0.52917720859,
        )

    shellinfo = simple_gbs_parser(
        filename=gbs_filename,
        symbols=symbols,
        )
    # The screening threshold used to filter out insignificant shell pairs by their overlap
    threshold_pq = 1.0e-14

    sizeof_double = np.dtype('double').itemsize

    # => Set Up cuEST <= #

    def cuest_check(
        title,
        return_code
        ):
        " A simple helper function to call cuEST functions and check the return code. "

        if return_code != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError(f"{title} failed with code {return_code}")

    # These are user-provided functions that are responsible for allocating
    # host and device workspace arrays.  We use
    persistent_workspace_descriptor = WorkspaceDescriptor()
    temporary_workspace_descriptor = WorkspaceDescriptor()

    cuest_handle_parameters = ce.cuestHandleParameters()

    cuest_check('Create Handle Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            outParameters=cuest_handle_parameters,
            )
        )

    # Here we allow cuEST to use the default stream, cuBLAS, etc., but those
    # parameters should be set in the handle parameters before this call.
    cuest_handle = ce.cuestHandle()
    cuest_check('Create Cuest Handle',
        ce.cuestCreate(
            parameters=cuest_handle_parameters,
            handle=cuest_handle,
            )
        )

    cuest_check('Destroy Handle Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_HANDLE_PARAMETERS,
            parameters=cuest_handle_parameters,
            )
        )

    aoshell_parameters = ce.cuestAOShellParameters()
    cuest_check('Create AO Shell Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            outParameters=aoshell_parameters,
            )
        )

    # => Build Gaussian Shells From Basis Parser Info <= #

    shells = []
    n_shells_per_atom = []
    shell_count = 1
    for atom_shells in shellinfo:
        n_shells_per_atom.append(len(atom_shells))
        for shell in atom_shells:
            L = shell['L']
            exponents = shell['exponents']
            raw_coefficients = shell['coefficients']
            normalized_coefficients = normalize_shell_coefficients(
                coefficients=raw_coefficients,
                exponents=exponents,
                L=L,
                normalization=shell['normalization'],
                )
            aoshell_handle = ce.cuestAOShellHandle()
            cuest_check(f'Create AO Shell {shell_count}',
                ce.cuestAOShellCreate(
                    handle=cuest_handle,
                    isPure=shell['is_pure'],
                    L=L,
                    numPrimitive=len(exponents),
                    exponents=exponents,
                    coefficients=normalized_coefficients,
                    parameters=aoshell_parameters,
                    outShell=aoshell_handle)
                )
            shells.append(aoshell_handle)
            shell_count += 1

    cuest_check('Destroy AO Shell Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            parameters=aoshell_parameters,
            )
        )

    # => Build Basis Set From Gaussian Shells <= #

    aobasis_parameters = ce.cuestAOBasisParameters()

    cuest_check('Create AO Basis Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            outParameters=aobasis_parameters,
            )
        )

    aobasis_handle = ce.cuestAOBasisHandle()

    cuest_check('Create AOBasis Workspace Query',
        ce.cuestAOBasisCreateWorkspaceQuery(
            handle=cuest_handle,
            numAtoms=len(symbols),
            numShellsPerAtom=n_shells_per_atom,
            shells=shells,
            parameters=aobasis_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outBasis=aobasis_handle,
            )
        )

    aobasis_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    aobasis_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create AOBasis',
        ce.cuestAOBasisCreate(
            handle=cuest_handle,
            numAtoms=len(symbols),
            numShellsPerAtom=n_shells_per_atom,
            shells=shells,
            parameters=aobasis_parameters,
            persistentWorkspace=aobasis_persistent_workspace.pointer,
            temporaryWorkspace=aobasis_temporary_workspace.pointer,
            outBasis=aobasis_handle,
            )
        )

    del aobasis_temporary_workspace

    for i, shell in enumerate(shells):
        cuest_check(f'Destroy AO Shell {i+1}',
            ce.cuestAOShellDestroy(
                handle=shell,
                )
            )
    aobasis_num_ao = ce.data_uint64_t()
    cuest_check('Query AO Basis Num AO',
        ce.cuestQuery(
            handle=cuest_handle,
            type=ce.CuestType.CUEST_AOBASIS,
            object=aobasis_handle,
            attribute=ce.CuestAOBasisAttributes.CUEST_AOBASIS_NUM_AO,
            attributeValue=aobasis_num_ao,
            )
        )
    nao = aobasis_num_ao.value

    cuest_check('Destroy AO Basis Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOBASIS_PARAMETERS,
            parameters=aobasis_parameters,
            )
        )

    # => Build Pair List <= #

    aopairlist_handle = ce.cuestAOPairListHandle()

    aopairlist_parameters = ce.cuestAOPairListParameters()

    cuest_check('Create AOPairList Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            outParameters=aopairlist_parameters,
            )
        )

    cuest_check('Create AOPairList Workspace Query',
        ce.cuestAOPairListCreateWorkspaceQuery(
            handle=cuest_handle,
            basis=aobasis_handle,
            numAtoms=len(symbols),
            xyz=xyzs,
            thresholdPQ=threshold_pq,
            parameters=aopairlist_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPairList=aopairlist_handle,
            )
        )

    aopairlist_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    aopairlist_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create AOPairList',
        ce.cuestAOPairListCreate(
            handle=cuest_handle,
            basis=aobasis_handle,
            numAtoms=len(symbols),
            xyz=xyzs,
            thresholdPQ=threshold_pq,
            parameters=aopairlist_parameters,
            persistentWorkspace=aopairlist_persistent_workspace.pointer,
            temporaryWorkspace=aopairlist_temporary_workspace.pointer,
            outPairList=aopairlist_handle,
            )
        )

    del aopairlist_temporary_workspace

    cuest_check('Destroy AOPairList Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_AOPAIRLIST_PARAMETERS,
            parameters=aopairlist_parameters,
            )
        )

    # => Build One Electron Integral Plan <= #

    oeintplan_handle = ce.cuestOEIntPlanHandle()
    oeintplan_parameters = ce.cuestOEIntPlanParameters()

    cuest_check('Create OEIntPlan Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_OEINTPLAN_PARAMETERS,
            outParameters=oeintplan_parameters,
            )
        )

    cuest_check('Create OEIntPlan Workspace Query',
        ce.cuestOEIntPlanCreateWorkspaceQuery(
            handle=cuest_handle,
            basis=aobasis_handle,
            pairList=aopairlist_handle,
            parameters=oeintplan_parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPlan=oeintplan_handle,
            )
        )

    oeintplan_persistent_workspace = Workspace(workspaceDescriptor=persistent_workspace_descriptor)
    oeintplan_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    cuest_check('Create OEIntPlan',
        ce.cuestOEIntPlanCreate(
            handle=cuest_handle,
            basis=aobasis_handle,
            pairList=aopairlist_handle,
            parameters=oeintplan_parameters,
            persistentWorkspace=oeintplan_persistent_workspace.pointer,
            temporaryWorkspace=oeintplan_temporary_workspace.pointer,
            outPlan=oeintplan_handle,
            )
        )

    del oeintplan_temporary_workspace

    cuest_check('Destroy OEIntPlan Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_OEINTPLAN_PARAMETERS,
            parameters=oeintplan_parameters,
            )
        )

    # => Overlap Integrals <= #

    overlapint_device_handle = ce.Pointer()
    compute_overlap_parameters = ce.cuestOverlapComputeParameters()
    cuest_check('Create Overlap Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_OVERLAPCOMPUTE_PARAMETERS,
            outParameters=compute_overlap_parameters,
            )
        )

    # Find Memory Requirements
    cuest_check('Compute Overlap Ints Workspace Query',
        ce.cuestOverlapComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_overlap_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outSMatrix=overlapint_device_handle,
            )
        )

    overlapint_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    overlapint_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * nao**2
        )
    overlapint_device_handle.value = np.intp(overlapint_device_pointer)

    # Compute Integrals
    cuest_check('Compute Overlap Ints',
        ce.cuestOverlapCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_overlap_parameters,
            temporaryWorkspace=overlapint_temporary_workspace.pointer,
            outSMatrix=overlapint_device_handle,
            )
        )

    del overlapint_temporary_workspace

    cuest_check('Destroy Overlap Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_OVERLAPCOMPUTE_PARAMETERS,
            parameters=compute_overlap_parameters,
            )
        )

    overlapint_host_array = np.empty(
        (nao * nao),
        dtype=np.double
        )

    cuda_memcpy_dtoh(
        host_pointer=overlapint_host_array.ctypes.data,
        device_pointer=overlapint_device_pointer,
        size_in_bytes=sizeof_double * nao**2,
        )

    cuda_free(
        array=overlapint_device_pointer
        )

    # => Kinetic Integrals <= #

    kineticint_device_handle = ce.Pointer()
    compute_kinetic_parameters = ce.cuestKineticComputeParameters()
    cuest_check('Create Kinetic Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_KINETICCOMPUTE_PARAMETERS,
            outParameters=compute_kinetic_parameters,
            )
        )

    # Find Memory Requirements
    cuest_check('Compute Kinetic Ints Workspace Query',
        ce.cuestKineticComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_kinetic_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outTMatrix=kineticint_device_handle,
            )
        )

    kineticint_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    kineticint_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * nao**2
        )
    kineticint_device_handle.value = np.intp(kineticint_device_pointer)

    # Compute Integrals
    cuest_check('Compute Kinetic Ints',
        ce.cuestKineticCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=compute_kinetic_parameters,
            temporaryWorkspace=kineticint_temporary_workspace.pointer,
            outTMatrix=kineticint_device_handle,
            )
        )

    del kineticint_temporary_workspace

    cuest_check('Destroy Kinetic Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_KINETICCOMPUTE_PARAMETERS,
            parameters=compute_kinetic_parameters,
            )
        )

    kineticint_host_array = np.empty(
        (nao * nao),
        dtype=np.double
        )

    cuda_memcpy_dtoh(
        host_pointer=kineticint_host_array.ctypes.data,
        device_pointer=kineticint_device_pointer,
        size_in_bytes=sizeof_double * nao**2,
        )

    cuda_free(
        array=kineticint_device_pointer
        )

    # => Potential Integrals <= #

    potentialint_device_handle = ce.Pointer()

    # Move the position and (negated) charges of the atoms to the GPU
    xyzs_device_handle = ce.Pointer()
    xyzs = np.array(xyzs)
    xyzs_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * len(xyzs)
        )
    xyzs_device_handle.value = np.intp(xyzs_device_pointer)
    cuda_memcpy_htod(
        device_pointer=xyzs_device_pointer,
        host_pointer=xyzs.ctypes.data,
        size_in_bytes=sizeof_double * len(xyzs),
        )

    # The integral compute in cuEST are electron repulsion integrals but we're intersted
    # in a nucleus in the ket here, so the Z values have been scaled by -1 in the parser
    Zs_device_handle = ce.Pointer()
    Zs = np.array(Zs)
    Zs_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * len(Zs)
        )
    Zs_device_handle.value = np.intp(Zs_device_pointer)
    cuda_memcpy_htod(
        device_pointer=Zs_device_pointer,
        host_pointer=Zs.ctypes.data,
        size_in_bytes=sizeof_double * len(Zs),
        )

    potential_compute_parameters = ce.cuestPotentialComputeParameters()
    cuest_check('Create Potential Params',
        ce.cuestParametersCreate(
            parametersType=ce.CuestParametersType.CUEST_POTENTIALCOMPUTE_PARAMETERS,
            outParameters=potential_compute_parameters,
            )
        )

    # Find Memory Requirements
    cuest_check('Compute potential Ints Workspace Query',
        ce.cuestPotentialComputeWorkspaceQuery(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=potential_compute_parameters,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            numCharges=len(symbols),
            xyz=xyzs_device_handle,
            q=Zs_device_handle,
            outVMatrix=potentialint_device_handle,
            )
        )

    potentialint_temporary_workspace = Workspace(workspaceDescriptor=temporary_workspace_descriptor)

    potentialint_device_pointer = cuda_malloc(
        size_in_bytes=sizeof_double * nao**2
        )
    potentialint_device_handle.value = np.intp(potentialint_device_pointer)

    # Compute Integrals
    cuest_check('Compute potential Ints',
        ce.cuestPotentialCompute(
            handle=cuest_handle,
            plan=oeintplan_handle,
            parameters=potential_compute_parameters,
            temporaryWorkspace=potentialint_temporary_workspace.pointer,
            numCharges=len(symbols),
            xyz=xyzs_device_handle,
            q=Zs_device_handle,
            outVMatrix=potentialint_device_handle,
            )
        )
    cuda_free(
        array=xyzs_device_pointer,
        )
    cuda_free(
        array=Zs_device_pointer,
        )

    del potentialint_temporary_workspace

    cuest_check('Destroy Potential Params',
        ce.cuestParametersDestroy(
            parametersType=ce.CuestParametersType.CUEST_POTENTIALCOMPUTE_PARAMETERS,
            parameters=potential_compute_parameters,
            )
        )

    potentialint_host_array = np.empty(
        (nao * nao),
        dtype=np.double
        )

    cuda_memcpy_dtoh(
        host_pointer=potentialint_host_array.ctypes.data,
        device_pointer=potentialint_device_pointer,
        size_in_bytes=sizeof_double * nao**2,
        )

    cuda_free(
        array=potentialint_device_pointer
        )

    # => Cleanup <= #

    cuest_check('Destroy OEIntPlan',
        ce.cuestOEIntPlanDestroy(
            handle=oeintplan_handle,
            )
        )

    cuest_check('Destroy AOPairList',
        ce.cuestAOPairListDestroy(
            handle=aopairlist_handle,
            )
        )

    cuest_check('Destroy AOBasis',
        ce.cuestAOBasisDestroy(
            handle=aobasis_handle,
            )
        )

    del oeintplan_persistent_workspace
    del aopairlist_persistent_workspace
    del aobasis_persistent_workspace

    cuest_check('Destroy Cuest Handle',
        ce.cuestDestroy(
            handle=cuest_handle,
            )
        )

if __name__ == "__main__":
    parser = argparse.ArgumentParser(
        description="Run using XYZ and GBS files."
        )
    parser.add_argument(
        "xyz_filename",
        type=str,
        help="The file with molecular coordinates (Angstrom) in basic xyz format."
    )
    parser.add_argument(
        "gbs_filename",
        type=str,
        help="The basis set file in G94/Psi4 GBS format."
    )

    args = parser.parse_args()

    run(
        xyz_filename=args.xyz_filename,