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

import cuest.bindings as ce

from .cuest_workspace_descriptor import CuestWorkspaceDescriptor
from .cuest_workspace import CuestWorkspace

from .cuest_handle import CuestHandle
from .cuest_ao_basis import CuestAOBasis
from .cuest_ao_pair_list import CuestAOPairList

from .cuest_parameters import CuestParameters

class CuestDFIntPlan(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        primary : CuestAOBasis,
        auxiliary : CuestAOBasis,
        ao_pair_list : CuestAOPairList,
        exchange_scale : float = 1.0,
        df_fitting_eigenvalue_cutoff : float = 1.0e-12,
        ):

        self.initialized = False

        self.handle = handle
        self.exchange_scale = exchange_scale
        self.df_fitting_eigenvalue_cutoff = df_fitting_eigenvalue_cutoff

        # NOTE: The CuestDFIntPlan has several defaultable parameters that
        # production codes may wish to override and expose as user-accessible
        # parameters.

        df_int_plan_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_DFINTPLAN_PARAMETERS,
            )

        exchange_scale_data = ce.data_double(self.exchange_scale)
        df_fitting_eigenvalue_cutoff_data = ce.data_double(self.df_fitting_eigenvalue_cutoff)

        self.df_int_plan_handle = ce.cuestDFIntPlanHandle()
        status = ce.cuestParametersConfigure(
            parametersType=ce.CuestParametersType.CUEST_DFINTPLAN_PARAMETERS,
            parameters=df_int_plan_parameters.parameters,
            attribute=ce.CuestDFIntPlanParametersAttributes.CUEST_DFINTPLAN_PARAMETERS_EXCHANGE_FRACTION,
            attributeValue=exchange_scale_data,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestParametersConfigure failed')

        status = ce.cuestParametersConfigure(
            parametersType=ce.CuestParametersType.CUEST_DFINTPLAN_PARAMETERS,
            parameters=df_int_plan_parameters.parameters,
            attribute=ce.CuestDFIntPlanParametersAttributes.CUEST_DFINTPLAN_PARAMETERS_FITTING_CUTOFF,
            attributeValue=df_fitting_eigenvalue_cutoff_data,
            )
        if status != ce.CuestStatus.CUEST_STATUS_SUCCESS:
            raise RuntimeError('cuestParametersConfigure failed')

        # => Workspace Query <= #

        persistent_workspace_descriptor = CuestWorkspaceDescriptor()
        temporary_workspace_descriptor = CuestWorkspaceDescriptor()

        status = ce.cuestDFIntPlanCreateWorkspaceQuery(
            handle=handle.handle,
            primaryBasis=primary.ao_basis_handle,
            auxiliaryBasis=auxiliary.ao_basis_handle,
            pairList=ao_pair_list.ao_pair_list_handle,
            parameters=df_int_plan_parameters.parameters,
            persistentWorkspaceDescriptor=persistent_workspace_descriptor.pointer,
            temporaryWorkspaceDescriptor=temporary_workspace_descriptor.pointer,
            outPlan=self.df_int_plan_handle,
            )

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

        # Most likely place to run out of device memory, so we will add a custom RuntimeError
        try:
            persistent_workspace = CuestWorkspace(workspaceDescriptor=persistent_workspace_descriptor)
            temporary_workspace = CuestWorkspace(workspaceDescriptor=temporary_workspace_descriptor)
        except RuntimeError as e:
            raise RuntimeError("CuestDFIntPlan workspace allocation failed - out of memory for CoreDF") from e

        # => Creation <= #

        status = ce.cuestDFIntPlanCreate(
            handle=handle.handle,
            primaryBasis=primary.ao_basis_handle,
            auxiliaryBasis=auxiliary.ao_basis_handle,
            pairList=ao_pair_list.ao_pair_list_handle,
            parameters=df_int_plan_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outPlan=self.df_int_plan_handle,
            )

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

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

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestDFIntPlanDestroy(
            handle=self.df_int_plan_handle,
            )

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


        status = ce.cuestDFIntPlanCreate(
            handle=handle.handle,
            primaryBasis=primary.ao_basis_handle,
            auxiliaryBasis=auxiliary.ao_basis_handle,
            pairList=ao_pair_list.ao_pair_list_handle,
            parameters=df_int_plan_parameters.parameters,
            persistentWorkspace=persistent_workspace.pointer,
            temporaryWorkspace=temporary_workspace.pointer,
            outPlan=self.df_int_plan_handle,
            )

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

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

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestDFIntPlanDestroy(
            handle=self.df_int_plan_handle,
            )

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


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

class UnitConversions(object):

    conversions = {
        'ang_per_bohr' : 0.52917720859, # PSI4
    }

    for key, value in conversions.copy().items():
        U1, U2 = key.split('_per_')
        key2 = U2 + '_per_' + U1
        conversions[key2] = 1.0 / value


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

import numpy as np

from .unit_conversions import UnitConversions
from .periodic_table import PeriodicTable

class Molecule(object):

    def __init__(
        self,
        *,
        N : np.ndarray,
        Z : np.ndarray,
        xyz : np.ndarray,
        ):

        self.N = N
        self.Z = Z
        self.xyz = xyz

        if self.N.shape != (self.natom,): raise RuntimeError('N.shape is not (natom,)')
        if self.Z.shape != (self.natom,): raise RuntimeError('Z.shape is not (natom,)')
        if self.xyz.shape != (self.natom, 3): raise RuntimeError('xyz.shape is not (natom, 3)')

        if self.N.dtype != np.uint64: raise RuntimeError('N.dtype is not np.uint64')
        if self.Z.dtype != np.float64: raise RuntimeError('Z.dtype is not np.float64')
        if self.xyz.dtype != np.float64: raise RuntimeError('xyz.dtype is not np.float64')

    @property
    def natom(self):
        return len(self.N)

    @staticmethod
    def parse_from_xyz_lines(
        lines,
        *,
        bohr_per_ang=UnitConversions.conversions['bohr_per_ang'],
        ):

        # Strip fringe blank lines (but not internal blank lines)
        lines = '\n'.join(lines).strip().split('\n')

        if len(lines) == 0:
            return Molecule(
                N=np.zeros((0,), dtype=np.uint64),
                Z=np.zeros((0,)),
                xyz=np.zeros((0, 3)),
                )

        import re

        # Natom line
        # (optional, but we enforce natom and length of file if specified)
        mobj = re.match(r'^\s*(\d+)\s*$', lines[0])
        if mobj:
            natom = int(mobj.group(1))
            lines = lines[2:]
            if len(lines) != natom:
                raise RuntimeError('natom line specified %d atoms, but %d lines remain in XYZ file' % (natom, len(lines)))

        re_atom_line = re.compile(r'^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$')

        Ns = []
        Zs = []
        xyzs = []

        for lindex, line in enumerate(lines):
            mobj = re.match(re_atom_line, line)
            if mobj is None:
                raise RuntimeError('Malformed xyz atom line #%d: %s' % (lindex, line))

            symbol = mobj.group(1)
            x = float(mobj.group(2))
            y = float(mobj.group(3))
            z = float(mobj.group(4))

            x *= bohr_per_ang
            y *= bohr_per_ang
            z *= bohr_per_ang

            # NOTE: This does not handle extravagant symbol names like "Gh(He)" or "OXT" or "H23"
            symbol_upper = symbol.upper()

            if symbol_upper not in PeriodicTable.symbol_upper_to_N_table:
                raise RuntimeError('Symbol %s is not in periodic table: %s' % (symbol_upper, line))

            N = PeriodicTable.symbol_upper_to_N_table[symbol_upper]
            Z = float(N)

            Ns.append(N)
            Zs.append(Z)
            xyzs.append((x, y, z))

        Ns = np.array(Ns, dtype=np.uint64)
        Zs = np.array(Zs)
        xyzs = np.array(xyzs)

        return Molecule(
            N=Ns,
            Z=Zs,
            xyz=xyzs,
            )

    @staticmethod
    def parse_from_xyz_string(
        string,
        **kwargs):

        return Molecule.parse_from_xyz_lines(lines=string.split('\n'), **kwargs)

    @staticmethod
    def parse_from_xyz_file(
        filename,
        **kwargs):

        with open(filename, 'r') as fh:
            lines = fh.read().splitlines()

        return Molecule.parse_from_xyz_lines(lines=lines, **kwargs)


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

import numpy as np

from .unit_conversions import UnitConversions
from .periodic_table import PeriodicTable

class Molecule(object):

    def __init__(
        self,
        *,
        N : np.ndarray,
        Z : np.ndarray,
        xyz : np.ndarray,
        ):

        self.N = N
        self.Z = Z
        self.xyz = xyz

        if self.N.shape != (self.natom,): raise RuntimeError('N.shape is not (natom,)')
        if self.Z.shape != (self.natom,): raise RuntimeError('Z.shape is not (natom,)')
        if self.xyz.shape != (self.natom, 3): raise RuntimeError('xyz.shape is not (natom, 3)')

        if self.N.dtype != np.uint64: raise RuntimeError('N.dtype is not np.uint64')
        if self.Z.dtype != np.float64: raise RuntimeError('Z.dtype is not np.float64')
        if self.xyz.dtype != np.float64: raise RuntimeError('xyz.dtype is not np.float64')

    @property
    def natom(self):
        return len(self.N)

    @staticmethod
    def parse_from_xyz_lines(
        lines,
        *,
        bohr_per_ang=UnitConversions.conversions['bohr_per_ang'],
        ):

        # Strip fringe blank lines (but not internal blank lines)
        lines = '\n'.join(lines).strip().split('\n')

        if len(lines) == 0:
            return Molecule(
                N=np.zeros((0,), dtype=np.uint64),
                Z=np.zeros((0,)),
                xyz=np.zeros((0, 3)),


natom

        self,
        *,
        N : np.ndarray,
        Z : np.ndarray,
        xyz : np.ndarray,
        ):

        self.N = N
        self.Z = Z
        self.xyz = xyz

        if self.N.shape != (self.natom,): raise RuntimeError('N.shape is not (natom,)')
        if self.Z.shape != (self.natom,): raise RuntimeError('Z.shape is not (natom,)')
        if self.xyz.shape != (self.natom, 3): raise RuntimeError('xyz.shape is not (natom, 3)')

        if self.N.dtype != np.uint64: raise RuntimeError('N.dtype is not np.uint64')
        if self.Z.dtype != np.float64: raise RuntimeError('Z.dtype is not np.float64')
        if self.xyz.dtype != np.float64: raise RuntimeError('xyz.dtype is not np.float64')

    @property
    def natom(self):
        return len(self.N)

    @staticmethod
    def parse_from_xyz_lines(
        lines,
        *,
        bohr_per_ang=UnitConversions.conversions['bohr_per_ang'],
        ):

        # Strip fringe blank lines (but not internal blank lines)
        lines = '\n'.join(lines).strip().split('\n')

        if len(lines) == 0:
            return Molecule(
                N=np.zeros((0,), dtype=np.uint64),
                Z=np.zeros((0,)),
                xyz=np.zeros((0, 3)),
                )

        import re


        xyz : np.ndarray,
        ):

        self.N = N
        self.Z = Z
        self.xyz = xyz

        if self.N.shape != (self.natom,): raise RuntimeError('N.shape is not (natom,)')
        if self.Z.shape != (self.natom,): raise RuntimeError('Z.shape is not (natom,)')
        if self.xyz.shape != (self.natom, 3): raise RuntimeError('xyz.shape is not (natom, 3)')

        if self.N.dtype != np.uint64: raise RuntimeError('N.dtype is not np.uint64')
        if self.Z.dtype != np.float64: raise RuntimeError('Z.dtype is not np.float64')
        if self.xyz.dtype != np.float64: raise RuntimeError('xyz.dtype is not np.float64')

    @property
    def natom(self):
        return len(self.N)

    @staticmethod
    def parse_from_xyz_lines(
        lines,
        *,
        bohr_per_ang=UnitConversions.conversions['bohr_per_ang'],
        ):

        # Strip fringe blank lines (but not internal blank lines)
        lines = '\n'.join(lines).strip().split('\n')

        if len(lines) == 0:
            return Molecule(
                N=np.zeros((0,), dtype=np.uint64),
                Z=np.zeros((0,)),
                xyz=np.zeros((0, 3)),
                )

        import re

        # Natom line
        # (optional, but we enforce natom and length of file if specified)
        mobj = re.match(r'^\s*(\d+)\s*$', lines[0])
        if mobj:
            natom = int(mobj.group(1))
            lines = lines[2:]
            if len(lines) != natom:
                raise RuntimeError('natom line specified %d atoms, but %d lines remain in XYZ file' % (natom, len(lines)))

        re_atom_line = re.compile(r'^\s*(\S+)\s+(\S+)\s+(\S+)\s+(\S+)\s*$')

        Ns = []
        Zs = []
        xyzs = []

        for lindex, line in enumerate(lines):
            mobj = re.match(re_atom_line, line)
            if mobj is None:
                raise RuntimeError('Malformed xyz atom line #%d: %s' % (lindex, line))

            symbol = mobj.group(1)
            x = float(mobj.group(2))
            y = float(mobj.group(3))
            z = float(mobj.group(4))

            x *= bohr_per_ang
            y *= bohr_per_ang
            z *= bohr_per_ang

            # NOTE: This does not handle extravagant symbol names like "Gh(He)" or "OXT" or "H23"
            symbol_upper = symbol.upper()

            if symbol_upper not in PeriodicTable.symbol_upper_to_N_table:
                raise RuntimeError('Symbol %s is not in periodic table: %s' % (symbol_upper, line))

            N = PeriodicTable.symbol_upper_to_N_table[symbol_upper]
            Z = float(N)

            Ns.append(N)
            Zs.append(Z)
            xyzs.append((x, y, z))

        Ns = np.array(Ns, dtype=np.uint64)
        Zs = np.array(Zs)
        xyzs = np.array(xyzs)

        return Molecule(
            N=Ns,
            Z=Zs,
            xyz=xyzs,
            )

    @staticmethod
    def parse_from_xyz_string(
        string,
        **kwargs):

        return Molecule.parse_from_xyz_lines(lines=string.split('\n'), **kwargs)

    @staticmethod
    def parse_from_xyz_file(
        filename,
        **kwargs):

        with open(filename, 'r') as fh:
            lines = fh.read().splitlines()

        return Molecule.parse_from_xyz_lines(lines=lines, **kwargs)


                raise RuntimeError('Symbol %s is not in periodic table: %s' % (symbol_upper, line))

            N = PeriodicTable.symbol_upper_to_N_table[symbol_upper]
            Z = float(N)

            Ns.append(N)
            Zs.append(Z)
            xyzs.append((x, y, z))

        Ns = np.array(Ns, dtype=np.uint64)
        Zs = np.array(Zs)
        xyzs = np.array(xyzs)

        return Molecule(
            N=Ns,
            Z=Zs,
            xyz=xyzs,
            )

    @staticmethod
    def parse_from_xyz_string(
        string,
        **kwargs):

        return Molecule.parse_from_xyz_lines(lines=string.split('\n'), **kwargs)

    @staticmethod
    def parse_from_xyz_file(
        filename,
        **kwargs):

        with open(filename, 'r') as fh:
            lines = fh.read().splitlines()

        return Molecule.parse_from_xyz_lines(lines=lines, **kwargs)


            xyzs.append((x, y, z))

        Ns = np.array(Ns, dtype=np.uint64)
        Zs = np.array(Zs)
        xyzs = np.array(xyzs)

        return Molecule(
            N=Ns,
            Z=Zs,
            xyz=xyzs,
            )

    @staticmethod
    def parse_from_xyz_string(
        string,
        **kwargs):

        return Molecule.parse_from_xyz_lines(lines=string.split('\n'), **kwargs)

    @staticmethod
    def parse_from_xyz_file(
        filename,
        **kwargs):

        with open(filename, 'r') as fh:
            lines = fh.read().splitlines()

        return Molecule.parse_from_xyz_lines(lines=lines, **kwargs)


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

import cuest.bindings as ce

from .ao_shell import AOShell

from .cuest_handle import CuestHandle

from .cuest_parameters import CuestParameters

class CuestAOShell(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        ao_shell : AOShell,
        ):

        self.initialized = False

        self.handle = handle

        ao_shell_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            )

        self.ao_shell_handle = ce.cuestAOShellHandle()

        status = ce.cuestAOShellCreate(
            handle=handle.handle,
            isPure=ao_shell.is_pure,
            L=ao_shell.L,
            numPrimitive=ao_shell.nprimitive,
            exponents=ao_shell.exponents,
            coefficients=ao_shell.coefficients,
            parameters=ao_shell_parameters.parameters,
            outShell=self.ao_shell_handle,
            )

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

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestAOShellDestroy(
            handle=self.ao_shell_handle,
            )

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

    # NOTE: Production codes may want to provide parameter queries here to
    # inspect the contents of the AOShell object


CuestAOShell

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

import cuest.bindings as ce

from .ao_shell import AOShell

from .cuest_handle import CuestHandle

from .cuest_parameters import CuestParameters

class CuestAOShell(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        ao_shell : AOShell,
        ):

        self.initialized = False

        self.handle = handle

        ao_shell_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            )

        self.ao_shell_handle = ce.cuestAOShellHandle()

        status = ce.cuestAOShellCreate(
            handle=handle.handle,
            isPure=ao_shell.is_pure,
            L=ao_shell.L,
            numPrimitive=ao_shell.nprimitive,
            exponents=ao_shell.exponents,
            coefficients=ao_shell.coefficients,
            parameters=ao_shell_parameters.parameters,
            outShell=self.ao_shell_handle,
            )

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

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestAOShellDestroy(
            handle=self.ao_shell_handle,
            )

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

    # NOTE: Production codes may want to provide parameter queries here to
    # inspect the contents of the AOShell object


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

import cuest.bindings as ce

from .ao_shell import AOShell

from .cuest_handle import CuestHandle

from .cuest_parameters import CuestParameters

class CuestAOShell(object):

    def __init__(
        self,
        *,
        handle : CuestHandle,
        ao_shell : AOShell,
        ):

        self.initialized = False

        self.handle = handle

        ao_shell_parameters = CuestParameters(
            parametersType=ce.CuestParametersType.CUEST_AOSHELL_PARAMETERS,
            )

        self.ao_shell_handle = ce.cuestAOShellHandle()

        status = ce.cuestAOShellCreate(
            handle=handle.handle,
            isPure=ao_shell.is_pure,
            L=ao_shell.L,
            numPrimitive=ao_shell.nprimitive,
            exponents=ao_shell.exponents,
            coefficients=ao_shell.coefficients,
            parameters=ao_shell_parameters.parameters,
            outShell=self.ao_shell_handle,
            )

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

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestAOShellDestroy(
            handle=self.ao_shell_handle,
            )

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

    # NOTE: Production codes may want to provide parameter queries here to
    # inspect the contents of the AOShell object


            )

        self.ao_shell_handle = ce.cuestAOShellHandle()

        status = ce.cuestAOShellCreate(
            handle=handle.handle,
            isPure=ao_shell.is_pure,
            L=ao_shell.L,
            numPrimitive=ao_shell.nprimitive,
            exponents=ao_shell.exponents,
            coefficients=ao_shell.coefficients,
            parameters=ao_shell_parameters.parameters,
            outShell=self.ao_shell_handle,
            )

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

        self.initialized = True

    def __del__(self):

        if not self.initialized: return

        status = ce.cuestAOShellDestroy(
            handle=self.ao_shell_handle,
            )

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

    # NOTE: Production codes may want to provide parameter queries here to
    # inspect the contents of the AOShell object


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

import numpy as np

from .cuda_utility import CudaUtility

class GPUMatrix(object):

    @staticmethod
    def from_numpy(cpu_array):

        assert len(cpu_array.shape) == 2
        matrix = GPUMatrix(
            nrows=cpu_array.shape[0],
            ncols=cpu_array.shape[1],
            dtype=cpu_array.dtype,
            )

        CudaUtility.cuda_memcpy_htod(
            device_pointer=matrix.pointer,
            host_pointer=cpu_array.ctypes.data,
            size_in_bytes=matrix.nrows*matrix.ncols*matrix.type_size,
            )

        return matrix

    @property
    def shape(self):
        return (self.nrows, self.ncols)

    def zero(self):
        CudaUtility.cuda_zero_memory(
            device_pointer=self.pointer,
            size_in_bytes=self.size*self.type_size,
            )

    def __init__(
        self,
        *,
        nrows,
        ncols,
        dtype=np.double,
        initialize=True,
        ):

        self.type_size = np.empty(1, dtype=dtype).itemsize
        self.nrows = nrows
        self.ncols = ncols
        self.dtype = dtype
        self.size = nrows * ncols

        self.pointer = CudaUtility.cuda_malloc(
            size_in_bytes=self.size*self.type_size,
            )

        if initialize:
            self.zero()

    def __del__(self):
        if hasattr(self, 'pointer') and self.pointer:
            CudaUtility.cuda_free(pointer=self.pointer)

    def to_numpy(self):
        cpu_array = np.empty(
            self.shape,
            dtype=self.dtype,
            )

        CudaUtility.cuda_memcpy_dtoh(
            host_pointer=cpu_array.ctypes.data,
            device_pointer=self.pointer,
            size_in_bytes=self.nrows*self.ncols*self.type_size,
            )

        return cpu_array

    def clone(self):
        matrix = GPUMatrix(
            nrows=self.nrows,
            ncols=self.ncols,
            dtype=self.dtype,
            )

        CudaUtility.cuda_memcpy_dtod(
            dst_device_pointer=matrix.pointer,
            src_device_pointer=self.pointer,
            size_in_bytes=self.nrows*self.ncols*self.type_size,
            )

        return matrix


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

import numpy as np

from .cuda_utility import CudaUtility

class GPUMatrix(object):

    @staticmethod
    def from_numpy(cpu_array):

        assert len(cpu_array.shape) == 2
        matrix = GPUMatrix(
            nrows=cpu_array.shape[0],
            ncols=cpu_array.shape[1],
            dtype=cpu_array.dtype,
            )

        CudaUtility.cuda_memcpy_htod(
            device_pointer=matrix.pointer,
            host_pointer=cpu_array.ctypes.data,
            size_in_bytes=matrix.nrows*matrix.ncols*matrix.type_size,
            )

        return matrix

    @property
    def shape(self):
        return (self.nrows, self.ncols)

    def zero(self):
        CudaUtility.cuda_zero_memory(
            device_pointer=self.pointer,
            size_in_bytes=self.size*self.type_size,
            )

    def __init__(
        self,
        *,
        nrows,
        ncols,
        dtype=np.double,
        initialize=True,
        ):


shape

    @staticmethod
    def from_numpy(cpu_array):

        assert len(cpu_array.shape) == 2
        matrix = GPUMatrix(
            nrows=cpu_array.shape[0],
            ncols=cpu_array.shape[1],
            dtype=cpu_array.dtype,
            )

        CudaUtility.cuda_memcpy_htod(
            device_pointer=matrix.pointer,
            host_pointer=cpu_array.ctypes.data,
            size_in_bytes=matrix.nrows*matrix.ncols*matrix.type_size,
            )

        return matrix

    @property
    def shape(self):
        return (self.nrows, self.ncols)

    def zero(self):
        CudaUtility.cuda_zero_memory(
            device_pointer=self.pointer,
            size_in_bytes=self.size*self.type_size,
            )

    def __init__(
        self,
        *,
        nrows,
        ncols,
        dtype=np.double,
        initialize=True,
        ):

        self.type_size = np.empty(1, dtype=dtype).itemsize
        self.nrows = nrows
        self.ncols = ncols
        self.dtype = dtype


zero

        assert len(cpu_array.shape) == 2
        matrix = GPUMatrix(
            nrows=cpu_array.shape[0],
            ncols=cpu_array.shape[1],
            dtype=cpu_array.dtype,
            )

        CudaUtility.cuda_memcpy_htod(
            device_pointer=matrix.pointer,
            host_pointer=cpu_array.ctypes.data,
            size_in_bytes=matrix.nrows*matrix.ncols*matrix.type_size,
            )

        return matrix

    @property
    def shape(self):
        return (self.nrows, self.ncols)

    def zero(self):
        CudaUtility.cuda_zero_memory(
            device_pointer=self.pointer,
            size_in_bytes=self.size*self.type_size,
            )

    def __init__(
        self,
        *,
        nrows,
        ncols,
        dtype=np.double,
        initialize=True,
        ):

        self.type_size = np.empty(1, dtype=dtype).itemsize
        self.nrows = nrows
        self.ncols = ncols
        self.dtype = dtype
        self.size = nrows * ncols

        self.pointer = CudaUtility.cuda_malloc(
            size_in_bytes=self.size*self.type_size,
            )


            )

        CudaUtility.cuda_memcpy_htod(
            device_pointer=matrix.pointer,
            host_pointer=cpu_array.ctypes.data,
            size_in_bytes=matrix.nrows*matrix.ncols*matrix.type_size,
            )

        return matrix

    @property
    def shape(self):
        return (self.nrows, self.ncols)

    def zero(self):
        CudaUtility.cuda_zero_memory(
            device_pointer=self.pointer,
            size_in_bytes=self.size*self.type_size,
            )

    def __init__(
        self,
        *,
        nrows,
        ncols,
        dtype=np.double,
        initialize=True,
        ):

        self.type_size = np.empty(1, dtype=dtype).itemsize
        self.nrows = nrows
        self.ncols = ncols
        self.dtype = dtype
        self.size = nrows * ncols

        self.pointer = CudaUtility.cuda_malloc(
            size_in_bytes=self.size*self.type_size,
            )

        if initialize:
            self.zero()

    def __del__(self):
        if hasattr(self, 'pointer') and self.pointer:
            CudaUtility.cuda_free(pointer=self.pointer)

    def to_numpy(self):
        cpu_array = np.empty(
            self.shape,
            dtype=self.dtype,
            )

        CudaUtility.cuda_memcpy_dtoh(
            host_pointer=cpu_array.ctypes.data,
            device_pointer=self.pointer,
            size_in_bytes=self.nrows*self.ncols*self.type_size,
            )

        return cpu_array


        nrows,
        ncols,
        dtype=np.double,
        initialize=True,
        ):

        self.type_size = np.empty(1, dtype=dtype).itemsize
        self.nrows = nrows
        self.ncols = ncols
        self.dtype = dtype
        self.size = nrows * ncols

        self.pointer = CudaUtility.cuda_malloc(
            size_in_bytes=self.size*self.type_size,
            )

        if initialize:
            self.zero()

    def __del__(self):
        if hasattr(self, 'pointer') and self.pointer:
            CudaUtility.cuda_free(pointer=self.pointer)

    def to_numpy(self):
        cpu_array = np.empty(
            self.shape,
            dtype=self.dtype,
            )

        CudaUtility.cuda_memcpy_dtoh(
            host_pointer=cpu_array.ctypes.data,
            device_pointer=self.pointer,
            size_in_bytes=self.nrows*self.ncols*self.type_size,
            )

        return cpu_array

    def clone(self):
        matrix = GPUMatrix(
            nrows=self.nrows,


        self.type_size = np.empty(1, dtype=dtype).itemsize
        self.nrows = nrows
        self.ncols = ncols
        self.dtype = dtype
        self.size = nrows * ncols

        self.pointer = CudaUtility.cuda_malloc(
            size_in_bytes=self.size*self.type_size,
            )

        if initialize:
            self.zero()

    def __del__(self):
        if hasattr(self, 'pointer') and self.pointer:
            CudaUtility.cuda_free(pointer=self.pointer)

    def to_numpy(self):
        cpu_array = np.empty(
            self.shape,
            dtype=self.dtype,
            )

        CudaUtility.cuda_memcpy_dtoh(
            host_pointer=cpu_array.ctypes.data,
            device_pointer=self.pointer,
            size_in_bytes=self.nrows*self.ncols*self.type_size,
            )

        return cpu_array

    def clone(self):
        matrix = GPUMatrix(
            nrows=self.nrows,
            ncols=self.ncols,
            dtype=self.dtype,
            )

        CudaUtility.cuda_memcpy_dtod(
            dst_device_pointer=matrix.pointer,
            src_device_pointer=self.pointer,
            size_in_bytes=self.nrows*self.ncols*self.type_size,
            )

        return matrix


clone

    def __del__(self):
        if hasattr(self, 'pointer') and self.pointer:
            CudaUtility.cuda_free(pointer=self.pointer)

    def to_numpy(self):
        cpu_array = np.empty(
            self.shape,
            dtype=self.dtype,
            )

        CudaUtility.cuda_memcpy_dtoh(
            host_pointer=cpu_array.ctypes.data,
            device_pointer=self.pointer,
            size_in_bytes=self.nrows*self.ncols*self.type_size,
            )

        return cpu_array

    def clone(self):
        matrix = GPUMatrix(
            nrows=self.nrows,
            ncols=self.ncols,
            dtype=self.dtype,
            )

        CudaUtility.cuda_memcpy_dtod(
            dst_device_pointer=matrix.pointer,
            src_device_pointer=self.pointer,
            size_in_bytes=self.nrows*self.ncols*self.type_size,
            )

        return matrix


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

import numpy as np

from .molecule import Molecule
from .ao_basis import AOBasis
from .sad_atom_structure import SADAtomStructure
from .sad_guess_atom import SADGuessAtom

from .memoized_property import memoized_property

class SADGuess(object):

    def __init__(
        self,
        *,
        sad_guess_atoms : list,
        ):

        self.sad_guess_atoms = sad_guess_atoms

        if not all(isinstance(_, SADGuessAtom) for _ in sad_guess_atoms): raise RuntimeError('sad_guess_atoms is not list of SADGuessAtom')

    @property
    def natom(self):
        return len(self.sad_guess_atoms)

    @memoized_property
    def primary(self):
        return AOBasis.concatenate(_.primary for _ in self.sad_guess_atoms)

    @memoized_property
    def minao(self):
        return AOBasis.concatenate(_.minao for _ in self.sad_guess_atoms)

    def compute_Cocc(self):
        Cocc = np.zeros((self.minao.nao, self.primary.nao))
        primary_start = 0
        minao_start = 0
        for A, sad_guess_atom in enumerate(self.sad_guess_atoms):
            nocc2 = sad_guess_atom.structure.nocc
            Cocc2 = sad_guess_atom.C
            Focc2 = np.einsum('i,ip->ip', np.sqrt(nocc2), Cocc2)
            nprimary = sad_guess_atom.primary.nao
            nminao = sad_guess_atom.minao.nao
            Cocc[minao_start:minao_start+nminao, primary_start:primary_start+nprimary] = Focc2
            primary_start += nprimary
            minao_start += nminao
        return Cocc

    def compute_Docc(self):
        Cocc = self.compute_Cocc()
        return np.dot(Cocc.T, Cocc)

    @staticmethod
    def build(
        *,
        molecule : Molecule,
        primary : AOBasis,
        minao : AOBasis,
        ):

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

        unique_sad_guess_atoms = {}

        structures = [SADAtomStructure.build(N=N) for N in molecule.N]
        primary_atoms = primary.atomize()
        minao_atoms = minao.atomize()

        for N, structure, primary_atom, minao_atom in zip(molecule.N, structures, primary_atoms, minao_atoms):
            if N not in unique_sad_guess_atoms:
                minao_atom2 = SADGuess.reorder_minao(
                    minao=minao_atom,
                    structure=structure,
                    )
                unique_sad_guess_atoms[N] = SADGuessAtom(
                    primary=primary_atom,
                    minao=minao_atom2,
                    structure=structure,
                    )

        return SADGuess(
            sad_guess_atoms=[unique_sad_guess_atoms[N] for N in molecule.N],
            )

    """
    Sometimes a GBS file is ordered in its own way, which does not correspond
    to the order of inactive and active shells in SADAtomStructure. E.g., an
    element like S might have its MINAO basis file ordered as 1s2s3s2p3p but
    the SADAtomStructure order is [1s2s2p][3s3p]. This function provides a sort
    of sheep and goats shuffle to transform a 1-atom AOBasis object to another
    1-atom AOBasis object with the order of structure, if this is topologically
    possible.
    """
    @staticmethod
    def reorder_minao(
        *,
        minao : AOBasis,
        structure : SADAtomStructure,
        ):

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

        if list(sorted(shell.L for shell in minao.shells[0])) != list(sorted(structure.Ls)):
            raise RuntimeError('minao and structure do not have topological agreement on L structure. N = %d' % (structure.N))

        back_map = {}
        Ns = [0 for L in range(minao.max_L+1)]
        index = 0
        for shell in minao.shells[0]:
            L = shell.L
            N = Ns[L]
            back_map[N, L] = index
            Ns[L] += 1
            index += 1

        shells = []
        Ns = [0 for L in range(minao.max_L+1)]
        for L in structure.Ls:
            N = Ns[L]
            index = back_map[N, L]
            shells.append(minao.shells[0][index].clone())
            Ns[L] += 1

        return AOBasis(shells=[shells])


SADGuess

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

import numpy as np

from .molecule import Molecule
from .ao_basis import AOBasis
from .sad_atom_structure import SADAtomStructure
from .sad_guess_atom import SADGuessAtom

from .memoized_property import memoized_property

class SADGuess(object):

    def __init__(
        self,
        *,
        sad_guess_atoms : list,
        ):

        self.sad_guess_atoms = sad_guess_atoms

        if not all(isinstance(_, SADGuessAtom) for _ in sad_guess_atoms): raise RuntimeError('sad_guess_atoms is not list of SADGuessAtom')

    @property
    def natom(self):
        return len(self.sad_guess_atoms)

    @memoized_property
    def primary(self):
        return AOBasis.concatenate(_.primary for _ in self.sad_guess_atoms)

    @memoized_property
    def minao(self):
        return AOBasis.concatenate(_.minao for _ in self.sad_guess_atoms)

    def compute_Cocc(self):
        Cocc = np.zeros((self.minao.nao, self.primary.nao))
        primary_start = 0
        minao_start = 0
        for A, sad_guess_atom in enumerate(self.sad_guess_atoms):
            nocc2 = sad_guess_atom.structure.nocc
            Cocc2 = sad_guess_atom.C
            Focc2 = np.einsum('i,ip->ip', np.sqrt(nocc2), Cocc2)
            nprimary = sad_guess_atom.primary.nao
            nminao = sad_guess_atom.minao.nao
            Cocc[minao_start:minao_start+nminao, primary_start:primary_start+nprimary] = Focc2
            primary_start += nprimary
            minao_start += nminao
        return Cocc

    def compute_Docc(self):
        Cocc = self.compute_Cocc()
        return np.dot(Cocc.T, Cocc)

    @staticmethod
    def build(
        *,
        molecule : Molecule,
        primary : AOBasis,
        minao : AOBasis,
        ):

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

        unique_sad_guess_atoms = {}

        structures = [SADAtomStructure.build(N=N) for N in molecule.N]
        primary_atoms = primary.atomize()
        minao_atoms = minao.atomize()

        for N, structure, primary_atom, minao_atom in zip(molecule.N, structures, primary_atoms, minao_atoms):
            if N not in unique_sad_guess_atoms:
                minao_atom2 = SADGuess.reorder_minao(
                    minao=minao_atom,
                    structure=structure,
                    )
                unique_sad_guess_atoms[N] = SADGuessAtom(
                    primary=primary_atom,
                    minao=minao_atom2,
                    structure=structure,
                    )

        return SADGuess(
            sad_guess_atoms=[unique_sad_guess_atoms[N] for N in molecule.N],
            )

    """
    Sometimes a GBS file is ordered in its own way, which does not correspond
    to the order of inactive and active shells in SADAtomStructure. E.g., an
    element like S might have its MINAO basis file ordered as 1s2s3s2p3p but
    the SADAtomStructure order is [1s2s2p][3s3p]. This function provides a sort
    of sheep and goats shuffle to transform a 1-atom AOBasis object to another
    1-atom AOBasis object with the order of structure, if this is topologically
    possible.
    """
    @staticmethod
    def reorder_minao(
        *,
        minao : AOBasis,
        structure : SADAtomStructure,
        ):

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

        if list(sorted(shell.L for shell in minao.shells[0])) != list(sorted(structure.Ls)):
            raise RuntimeError('minao and structure do not have topological agreement on L structure. N = %d' % (structure.N))

        back_map = {}
        Ns = [0 for L in range(minao.max_L+1)]
        index = 0
        for shell in minao.shells[0]:
            L = shell.L
            N = Ns[L]
            back_map[N, L] = index
            Ns[L] += 1
            index += 1

        shells = []
        Ns = [0 for L in range(minao.max_L+1)]
        for L in structure.Ls:
            N = Ns[L]
            index = back_map[N, L]
            shells.append(minao.shells[0][index].clone())
            Ns[L] += 1

        return AOBasis(shells=[shells])


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

import numpy as np

from .molecule import Molecule
from .ao_basis import AOBasis
from .sad_atom_structure import SADAtomStructure
from .sad_guess_atom import SADGuessAtom

from .memoized_property import memoized_property

class SADGuess(object):

    def __init__(
        self,
        *,
        sad_guess_atoms : list,
        ):

        self.sad_guess_atoms = sad_guess_atoms

        if not all(isinstance(_, SADGuessAtom) for _ in sad_guess_atoms): raise RuntimeError('sad_guess_atoms is not list of SADGuessAtom')

    @property
    def natom(self):
        return len(self.sad_guess_atoms)

    @memoized_property
    def primary(self):
        return AOBasis.concatenate(_.primary for _ in self.sad_guess_atoms)

    @memoized_property
    def minao(self):
        return AOBasis.concatenate(_.minao for _ in self.sad_guess_atoms)

    def compute_Cocc(self):
        Cocc = np.zeros((self.minao.nao, self.primary.nao))
        primary_start = 0
        minao_start = 0
        for A, sad_guess_atom in enumerate(self.sad_guess_atoms):
            nocc2 = sad_guess_atom.structure.nocc
            Cocc2 = sad_guess_atom.C


natom

from .molecule import Molecule
from .ao_basis import AOBasis
from .sad_atom_structure import SADAtomStructure
from .sad_guess_atom import SADGuessAtom

from .memoized_property import memoized_property

class SADGuess(object):

    def __init__(
        self,
        *,
        sad_guess_atoms : list,
        ):

        self.sad_guess_atoms = sad_guess_atoms

        if not all(isinstance(_, SADGuessAtom) for _ in sad_guess_atoms): raise RuntimeError('sad_guess_atoms is not list of SADGuessAtom')

    @property
    def natom(self):
        return len(self.sad_guess_atoms)

    @memoized_property
    def primary(self):
        return AOBasis.concatenate(_.primary for _ in self.sad_guess_atoms)

    @memoized_property
    def minao(self):
        return AOBasis.concatenate(_.minao for _ in self.sad_guess_atoms)

    def compute_Cocc(self):
        Cocc = np.zeros((self.minao.nao, self.primary.nao))
        primary_start = 0
        minao_start = 0
        for A, sad_guess_atom in enumerate(self.sad_guess_atoms):
            nocc2 = sad_guess_atom.structure.nocc
            Cocc2 = sad_guess_atom.C
            Focc2 = np.einsum('i,ip->ip', np.sqrt(nocc2), Cocc2)
            nprimary = sad_guess_atom.primary.nao
            nminao = sad_guess_atom.minao.nao
            Cocc[minao_start:minao_start+nminao, primary_start:primary_start+nprimary] = Focc2


primary

from .memoized_property import memoized_property

class SADGuess(object):

    def __init__(
        self,
        *,
        sad_guess_atoms : list,
        ):

        self.sad_guess_atoms = sad_guess_atoms

        if not all(isinstance(_, SADGuessAtom) for _ in sad_guess_atoms): raise RuntimeError('sad_guess_atoms is not list of SADGuessAtom')

    @property
    def natom(self):
        return len(self.sad_guess_atoms)

    @memoized_property
    def primary(self):
        return AOBasis.concatenate(_.primary for _ in self.sad_guess_atoms)

    @memoized_property
    def minao(self):
        return AOBasis.concatenate(_.minao for _ in self.sad_guess_atoms)

    def compute_Cocc(self):
        Cocc = np.zeros((self.minao.nao, self.primary.nao))
        primary_start = 0
        minao_start = 0
        for A, sad_guess_atom in enumerate(self.sad_guess_atoms):
            nocc2 = sad_guess_atom.structure.nocc
            Cocc2 = sad_guess_atom.C
            Focc2 = np.einsum('i,ip->ip', np.sqrt(nocc2), Cocc2)
            nprimary = sad_guess_atom.primary.nao
            nminao = sad_guess_atom.minao.nao
            Cocc[minao_start:minao_start+nminao, primary_start:primary_start+nprimary] = Focc2
            primary_start += nprimary
            minao_start += nminao
        return Cocc


minao

    def __init__(
        self,
        *,
        sad_guess_atoms : list,
        ):

        self.sad_guess_atoms = sad_guess_atoms

        if not all(isinstance(_, SADGuessAtom) for _ in sad_guess_atoms): raise RuntimeError('sad_guess_atoms is not list of SADGuessAtom')

    @property
    def natom(self):
        return len(self.sad_guess_atoms)

    @memoized_property
    def primary(self):
        return AOBasis.concatenate(_.primary for _ in self.sad_guess_atoms)

    @memoized_property
    def minao(self):
        return AOBasis.concatenate(_.minao for _ in self.sad_guess_atoms)

    def compute_Cocc(self):
        Cocc = np.zeros((self.minao.nao, self.primary.nao))
        primary_start = 0
        minao_start = 0
        for A, sad_guess_atom in enumerate(self.sad_guess_atoms):
            nocc2 = sad_guess_atom.structure.nocc
            Cocc2 = sad_guess_atom.C
            Focc2 = np.einsum('i,ip->ip', np.sqrt(nocc2), Cocc2)
            nprimary = sad_guess_atom.primary.nao
            nminao = sad_guess_atom.minao.nao
            Cocc[minao_start:minao_start+nminao, primary_start:primary_start+nprimary] = Focc2
            primary_start += nprimary
            minao_start += nminao
        return Cocc

    def compute_Docc(self):
        Cocc = self.compute_Cocc()
        return np.dot(Cocc.T, Cocc)


        *,
        sad_guess_atoms : list,
        ):

        self.sad_guess_atoms = sad_guess_atoms

        if not all(isinstance(_, SADGuessAtom) for _ in sad_guess_atoms): raise RuntimeError('sad_guess_atoms is not list of SADGuessAtom')

    @property
    def natom(self):
        return len(self.sad_guess_atoms)

    @memoized_property
    def primary(self):
        return AOBasis.concatenate(_.primary for _ in self.sad_guess_atoms)

    @memoized_property
    def minao(self):
        return AOBasis.concatenate(_.minao for _ in self.sad_guess_atoms)

    def compute_Cocc(self):
        Cocc = np.zeros((self.minao.nao, self.primary.nao))
        primary_start = 0
        minao_start = 0
        for A, sad_guess_atom in enumerate(self.sad_guess_atoms):
            nocc2 = sad_guess_atom.structure.nocc
            Cocc2 = sad_guess_atom.C
            Focc2 = np.einsum('i,ip->ip', np.sqrt(nocc2), Cocc2)
            nprimary = sad_guess_atom.primary.nao
            nminao = sad_guess_atom.minao.nao
            Cocc[minao_start:minao_start+nminao, primary_start:primary_start+nprimary] = Focc2
            primary_start += nprimary
            minao_start += nminao
        return Cocc

    def compute_Docc(self):
        Cocc = self.compute_Cocc()
        return np.dot(Cocc.T, Cocc)

    @staticmethod
    def build(
        *,
        molecule : Molecule,
        primary : AOBasis,
        minao : AOBasis,
        ):

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

        unique_sad_guess_atoms = {}

        structures = [SADAtomStructure.build(N=N) for N in molecule.N]
        primary_atoms = primary.atomize()


    @memoized_property
    def minao(self):
        return AOBasis.concatenate(_.minao for _ in self.sad_guess_atoms)

    def compute_Cocc(self):
        Cocc = np.zeros((self.minao.nao, self.primary.nao))
        primary_start = 0
        minao_start = 0
        for A, sad_guess_atom in enumerate(self.sad_guess_atoms):
            nocc2 = sad_guess_atom.structure.nocc
            Cocc2 = sad_guess_atom.C
            Focc2 = np.einsum('i,ip->ip', np.sqrt(nocc2), Cocc2)
            nprimary = sad_guess_atom.primary.nao
            nminao = sad_guess_atom.minao.nao
            Cocc[minao_start:minao_start+nminao, primary_start:primary_start+nprimary] = Focc2
            primary_start += nprimary
            minao_start += nminao
        return Cocc

    def compute_Docc(self):
        Cocc = self.compute_Cocc()
        return np.dot(Cocc.T, Cocc)

    @staticmethod
    def build(
        *,
        molecule : Molecule,
        primary : AOBasis,
        minao : AOBasis,
        ):

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

        unique_sad_guess_atoms = {}

        structures = [SADAtomStructure.build(N=N) for N in molecule.N]
        primary_atoms = primary.atomize()
        minao_atoms = minao.atomize()

        for N, structure, primary_atom, minao_atom in zip(molecule.N, structures, primary_atoms, minao_atoms):
            if N not in unique_sad_guess_atoms:


build

    def compute_Cocc(self):
        Cocc = np.zeros((self.minao.nao, self.primary.nao))
        primary_start = 0
        minao_start = 0
        for A, sad_guess_atom in enumerate(self.sad_guess_atoms):
            nocc2 = sad_guess_atom.structure.nocc
            Cocc2 = sad_guess_atom.C
            Focc2 = np.einsum('i,ip->ip', np.sqrt(nocc2), Cocc2)
            nprimary = sad_guess_atom.primary.nao
            nminao = sad_guess_atom.minao.nao
            Cocc[minao_start:minao_start+nminao, primary_start:primary_start+nprimary] = Focc2
            primary_start += nprimary
            minao_start += nminao
        return Cocc

    def compute_Docc(self):
        Cocc = self.compute_Cocc()
        return np.dot(Cocc.T, Cocc)

    @staticmethod
    def build(
        *,
        molecule : Molecule,
        primary : AOBasis,
        minao : AOBasis,
        ):

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

        unique_sad_guess_atoms = {}

        structures = [SADAtomStructure.build(N=N) for N in molecule.N]
        primary_atoms = primary.atomize()
        minao_atoms = minao.atomize()

        for N, structure, primary_atom, minao_atom in zip(molecule.N, structures, primary_atoms, minao_atoms):
            if N not in unique_sad_guess_atoms:
                minao_atom2 = SADGuess.reorder_minao(
                    minao=minao_atom,
                    structure=structure,
                    )
                unique_sad_guess_atoms[N] = SADGuessAtom(
                    primary=primary_atom,
                    minao=minao_atom2,
                    structure=structure,
                    )

        return SADGuess(
            sad_guess_atoms=[unique_sad_guess_atoms[N] for N in molecule.N],
            )

    """
    Sometimes a GBS file is ordered in its own way, which does not correspond
    to the order of inactive and active shells in SADAtomStructure. E.g., an
    element like S might have its MINAO basis file ordered as 1s2s3s2p3p but
    the SADAtomStructure order is [1s2s2p][3s3p]. This function provides a sort
    of sheep and goats shuffle to transform a 1-atom AOBasis object to another
    1-atom AOBasis object with the order of structure, if this is topologically
    possible.
    """
    @staticmethod
    def reorder_minao(
        *,
        minao : AOBasis,
        structure : SADAtomStructure,
        ):

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

        if list(sorted(shell.L for shell in minao.shells[0])) != list(sorted(structure.Ls)):


                unique_sad_guess_atoms[N] = SADGuessAtom(
                    primary=primary_atom,
                    minao=minao_atom2,
                    structure=structure,
                    )

        return SADGuess(
            sad_guess_atoms=[unique_sad_guess_atoms[N] for N in molecule.N],
            )

    """
    Sometimes a GBS file is ordered in its own way, which does not correspond
    to the order of inactive and active shells in SADAtomStructure. E.g., an
    element like S might have its MINAO basis file ordered as 1s2s3s2p3p but
    the SADAtomStructure order is [1s2s2p][3s3p]. This function provides a sort
    of sheep and goats shuffle to transform a 1-atom AOBasis object to another
    1-atom AOBasis object with the order of structure, if this is topologically
    possible.
    """
    @staticmethod
    def reorder_minao(
        *,
        minao : AOBasis,
        structure : SADAtomStructure,
        ):

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

        if list(sorted(shell.L for shell in minao.shells[0])) != list(sorted(structure.Ls)):
            raise RuntimeError('minao and structure do not have topological agreement on L structure. N = %d' % (structure.N))

        back_map = {}
        Ns = [0 for L in range(minao.max_L+1)]
        index = 0
        for shell in minao.shells[0]:
            L = shell.L
            N = Ns[L]
            back_map[N, L] = index
            Ns[L] += 1
            index += 1

        shells = []
        Ns = [0 for L in range(minao.max_L+1)]
        for L in structure.Ls:
            N = Ns[L]
            index = back_map[N, L]
            shells.append(minao.shells[0][index].clone())
            Ns[L] += 1

        return AOBasis(shells=[shells])


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

import numpy as np

class AOShell(object):

    def __init__(
        self,
        *,
        is_pure : bool,
        L : int,
        exponents : np.ndarray,
        coefficients : np.ndarray,
        ):

        self.is_pure = is_pure
        self.L = L
        self.exponents = exponents
        self.coefficients = coefficients

        if exponents.shape != (self.nprimitive,): raise RuntimeError('exponents.shape != (nprimitive,)')
        if coefficients.shape != (self.nprimitive,): raise RuntimeError('coefficients.shape != (nprimitive,)')

        if L < 0: raise RuntimeError('L < 0')

    @property
    def nprimitive(self):
        return len(self.exponents)

    @property
    def npure(self):
        return 2 * self.L + 1

    @property
    def ncart(self):
        return (self.L + 1) * (self.L + 2) // 2

    @property
    def nao(self):
        return self.npure if self.is_pure else self.ncart

    def clone(self):
        return AOShell(
            is_pure=self.is_pure,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_pure(self):
        return AOShell(
            is_pure=True,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_cart(self):
        return AOShell(
            is_pure=False,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def __eq__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return False
        if self.nprimitive != other.nprimitive: return False
        if self.is_pure != other.is_pure: return False
        if any(self.exponents != other.exponents): return False
        if any(self.coefficients != other.coefficients): return False
        return True

    def __ne__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return True
        if self.nprimitive != other.nprimitive: return True
        if self.is_pure != other.is_pure: return True
        if any(self.exponents != other.exponents): return True
        if any(self.coefficients != other.coefficients): return True
        return False

    @staticmethod
    def compute_normalized_coefficients(
        *,
        L,
        exponents,
        coefficients_raw,
        normalization=1.0,
        ):

        dfact = 1
        for l in range(1,L+1):
            dfact *= 2*l-1

        coefficients = coefficients_raw * np.sqrt(2**L * (2.0 * exponents)**(L + 1.5) / (np.pi**(1.5) * dfact))

        V = 0.0
        for k1 in range(len(coefficients)):
            for k2 in range(len(coefficients)):
                V += (np.sqrt(4.0 * exponents[k1] * exponents[k2]) / (exponents[k1] + exponents[k2]))**(L + 1.5) * coefficients_raw[k1] * coefficients_raw[k2]

        coefficients *= np.sqrt(normalization / V)

        return coefficients

    @staticmethod
    def build_from_raw_data(
        *,
        is_pure,
        L,
        exponents,
        coefficients_raw,
        normalization=1.0,
        ):

        coefficients = AOShell.compute_normalized_coefficients(
            L=L,
            exponents=exponents,
            coefficients_raw=coefficients_raw,
            normalization=normalization,
            )

        return AOShell(
            is_pure=is_pure,
            L=L,
            exponents=exponents,
            coefficients=coefficients,
            )


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

import numpy as np

class AOShell(object):

    def __init__(
        self,
        *,
        is_pure : bool,
        L : int,
        exponents : np.ndarray,
        coefficients : np.ndarray,
        ):

        self.is_pure = is_pure
        self.L = L
        self.exponents = exponents
        self.coefficients = coefficients

        if exponents.shape != (self.nprimitive,): raise RuntimeError('exponents.shape != (nprimitive,)')
        if coefficients.shape != (self.nprimitive,): raise RuntimeError('coefficients.shape != (nprimitive,)')

        if L < 0: raise RuntimeError('L < 0')

    @property
    def nprimitive(self):
        return len(self.exponents)

    @property
    def npure(self):
        return 2 * self.L + 1

    @property
    def ncart(self):
        return (self.L + 1) * (self.L + 2) // 2

    @property
    def nao(self):
        return self.npure if self.is_pure else self.ncart

    def clone(self):
        return AOShell(
            is_pure=self.is_pure,


nprimitive

    def __init__(
        self,
        *,
        is_pure : bool,
        L : int,
        exponents : np.ndarray,
        coefficients : np.ndarray,
        ):

        self.is_pure = is_pure
        self.L = L
        self.exponents = exponents
        self.coefficients = coefficients

        if exponents.shape != (self.nprimitive,): raise RuntimeError('exponents.shape != (nprimitive,)')
        if coefficients.shape != (self.nprimitive,): raise RuntimeError('coefficients.shape != (nprimitive,)')

        if L < 0: raise RuntimeError('L < 0')

    @property
    def nprimitive(self):
        return len(self.exponents)

    @property
    def npure(self):
        return 2 * self.L + 1

    @property
    def ncart(self):
        return (self.L + 1) * (self.L + 2) // 2

    @property
    def nao(self):
        return self.npure if self.is_pure else self.ncart

    def clone(self):
        return AOShell(
            is_pure=self.is_pure,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )


npure

        L : int,
        exponents : np.ndarray,
        coefficients : np.ndarray,
        ):

        self.is_pure = is_pure
        self.L = L
        self.exponents = exponents
        self.coefficients = coefficients

        if exponents.shape != (self.nprimitive,): raise RuntimeError('exponents.shape != (nprimitive,)')
        if coefficients.shape != (self.nprimitive,): raise RuntimeError('coefficients.shape != (nprimitive,)')

        if L < 0: raise RuntimeError('L < 0')

    @property
    def nprimitive(self):
        return len(self.exponents)

    @property
    def npure(self):
        return 2 * self.L + 1

    @property
    def ncart(self):
        return (self.L + 1) * (self.L + 2) // 2

    @property
    def nao(self):
        return self.npure if self.is_pure else self.ncart

    def clone(self):
        return AOShell(
            is_pure=self.is_pure,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_pure(self):
        return AOShell(
            is_pure=True,


ncart

        self.is_pure = is_pure
        self.L = L
        self.exponents = exponents
        self.coefficients = coefficients

        if exponents.shape != (self.nprimitive,): raise RuntimeError('exponents.shape != (nprimitive,)')
        if coefficients.shape != (self.nprimitive,): raise RuntimeError('coefficients.shape != (nprimitive,)')

        if L < 0: raise RuntimeError('L < 0')

    @property
    def nprimitive(self):
        return len(self.exponents)

    @property
    def npure(self):
        return 2 * self.L + 1

    @property
    def ncart(self):
        return (self.L + 1) * (self.L + 2) // 2

    @property
    def nao(self):
        return self.npure if self.is_pure else self.ncart

    def clone(self):
        return AOShell(
            is_pure=self.is_pure,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_pure(self):
        return AOShell(
            is_pure=True,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )


nao

        self.coefficients = coefficients

        if exponents.shape != (self.nprimitive,): raise RuntimeError('exponents.shape != (nprimitive,)')
        if coefficients.shape != (self.nprimitive,): raise RuntimeError('coefficients.shape != (nprimitive,)')

        if L < 0: raise RuntimeError('L < 0')

    @property
    def nprimitive(self):
        return len(self.exponents)

    @property
    def npure(self):
        return 2 * self.L + 1

    @property
    def ncart(self):
        return (self.L + 1) * (self.L + 2) // 2

    @property
    def nao(self):
        return self.npure if self.is_pure else self.ncart

    def clone(self):
        return AOShell(
            is_pure=self.is_pure,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_pure(self):
        return AOShell(
            is_pure=True,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_cart(self):
        return AOShell(
            is_pure=False,


clone

        if coefficients.shape != (self.nprimitive,): raise RuntimeError('coefficients.shape != (nprimitive,)')

        if L < 0: raise RuntimeError('L < 0')

    @property
    def nprimitive(self):
        return len(self.exponents)

    @property
    def npure(self):
        return 2 * self.L + 1

    @property
    def ncart(self):
        return (self.L + 1) * (self.L + 2) // 2

    @property
    def nao(self):
        return self.npure if self.is_pure else self.ncart

    def clone(self):
        return AOShell(
            is_pure=self.is_pure,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_pure(self):
        return AOShell(
            is_pure=True,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_cart(self):
        return AOShell(
            is_pure=False,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def __eq__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented


    @property
    def npure(self):
        return 2 * self.L + 1

    @property
    def ncart(self):
        return (self.L + 1) * (self.L + 2) // 2

    @property
    def nao(self):
        return self.npure if self.is_pure else self.ncart

    def clone(self):
        return AOShell(
            is_pure=self.is_pure,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_pure(self):
        return AOShell(
            is_pure=True,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_cart(self):
        return AOShell(
            is_pure=False,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def __eq__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return False
        if self.nprimitive != other.nprimitive: return False
        if self.is_pure != other.is_pure: return False
        if any(self.exponents != other.exponents): return False
        if any(self.coefficients != other.coefficients): return False
        return True

    def __ne__(self, other):


    @property
    def nao(self):
        return self.npure if self.is_pure else self.ncart

    def clone(self):
        return AOShell(
            is_pure=self.is_pure,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_pure(self):
        return AOShell(
            is_pure=True,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_cart(self):
        return AOShell(
            is_pure=False,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def __eq__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return False
        if self.nprimitive != other.nprimitive: return False
        if self.is_pure != other.is_pure: return False
        if any(self.exponents != other.exponents): return False
        if any(self.coefficients != other.coefficients): return False
        return True

    def __ne__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return True
        if self.nprimitive != other.nprimitive: return True
        if self.is_pure != other.is_pure: return True
        if any(self.exponents != other.exponents): return True
        if any(self.coefficients != other.coefficients): return True
        return False


            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_pure(self):
        return AOShell(
            is_pure=True,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def update_to_cart(self):
        return AOShell(
            is_pure=False,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def __eq__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return False
        if self.nprimitive != other.nprimitive: return False
        if self.is_pure != other.is_pure: return False
        if any(self.exponents != other.exponents): return False
        if any(self.coefficients != other.coefficients): return False
        return True

    def __ne__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return True
        if self.nprimitive != other.nprimitive: return True
        if self.is_pure != other.is_pure: return True
        if any(self.exponents != other.exponents): return True
        if any(self.coefficients != other.coefficients): return True
        return False

    @staticmethod
    def compute_normalized_coefficients(
        *,
        L,
        exponents,
        coefficients_raw,
        normalization=1.0,
        ):


            )

    def update_to_cart(self):
        return AOShell(
            is_pure=False,
            L=self.L,
            exponents=self.exponents.copy(),
            coefficients=self.coefficients.copy(),
            )

    def __eq__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return False
        if self.nprimitive != other.nprimitive: return False
        if self.is_pure != other.is_pure: return False
        if any(self.exponents != other.exponents): return False
        if any(self.coefficients != other.coefficients): return False
        return True

    def __ne__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return True
        if self.nprimitive != other.nprimitive: return True
        if self.is_pure != other.is_pure: return True
        if any(self.exponents != other.exponents): return True
        if any(self.coefficients != other.coefficients): return True
        return False

    @staticmethod
    def compute_normalized_coefficients(
        *,
        L,
        exponents,
        coefficients_raw,
        normalization=1.0,
        ):

        dfact = 1
        for l in range(1,L+1):
            dfact *= 2*l-1

        coefficients = coefficients_raw * np.sqrt(2**L * (2.0 * exponents)**(L + 1.5) / (np.pi**(1.5) * dfact))

        V = 0.0
        for k1 in range(len(coefficients)):
            for k2 in range(len(coefficients)):
                V += (np.sqrt(4.0 * exponents[k1] * exponents[k2]) / (exponents[k1] + exponents[k2]))**(L + 1.5) * coefficients_raw[k1] * coefficients_raw[k2]


        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return False
        if self.nprimitive != other.nprimitive: return False
        if self.is_pure != other.is_pure: return False
        if any(self.exponents != other.exponents): return False
        if any(self.coefficients != other.coefficients): return False
        return True

    def __ne__(self, other):
        if not isinstance(other, AOShell):
            return NotImplemented
        if self.L != other.L: return True
        if self.nprimitive != other.nprimitive: return True
        if self.is_pure != other.is_pure: return True
        if any(self.exponents != other.exponents): return True
        if any(self.coefficients != other.coefficients): return True
        return False

    @staticmethod
    def compute_normalized_coefficients(
        *,
        L,
        exponents,
        coefficients_raw,
        normalization=1.0,
        ):

        dfact = 1
        for l in range(1,L+1):
            dfact *= 2*l-1

        coefficients = coefficients_raw * np.sqrt(2**L * (2.0 * exponents)**(L + 1.5) / (np.pi**(1.5) * dfact))

        V = 0.0
        for k1 in range(len(coefficients)):
            for k2 in range(len(coefficients)):
                V += (np.sqrt(4.0 * exponents[k1] * exponents[k2]) / (exponents[k1] + exponents[k2]))**(L + 1.5) * coefficients_raw[k1] * coefficients_raw[k2]

        coefficients *= np.sqrt(normalization / V)

        return coefficients

    @staticmethod
    def build_from_raw_data(
        *,
        is_pure,
        L,
        exponents,
        coefficients_raw,
        normalization=1.0,
        ):

        coefficients = AOShell.compute_normalized_coefficients(
            L=L,
            exponents=exponents,
            coefficients_raw=coefficients_raw,
            normalization=normalization,
            )

        return AOShell(
            is_pure=is_pure,


        coefficients_raw,
        normalization=1.0,
        ):

        dfact = 1
        for l in range(1,L+1):
            dfact *= 2*l-1

        coefficients = coefficients_raw * np.sqrt(2**L * (2.0 * exponents)**(L + 1.5) / (np.pi**(1.5) * dfact))

        V = 0.0
        for k1 in range(len(coefficients)):
            for k2 in range(len(coefficients)):
                V += (np.sqrt(4.0 * exponents[k1] * exponents[k2]) / (exponents[k1] + exponents[k2]))**(L + 1.5) * coefficients_raw[k1] * coefficients_raw[k2]

        coefficients *= np.sqrt(normalization / V)

        return coefficients

    @staticmethod
    def build_from_raw_data(
        *,
        is_pure,
        L,
        exponents,
        coefficients_raw,
        normalization=1.0,
        ):

        coefficients = AOShell.compute_normalized_coefficients(
            L=L,
            exponents=exponents,
            coefficients_raw=coefficients_raw,
            normalization=normalization,
            )

        return AOShell(
            is_pure=is_pure,
            L=L,
            exponents=exponents,
            coefficients=coefficients,
            )


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

import numpy as np

from .periodic_table import PeriodicTable

from .ao_shell import AOShell

from .memoized_property import memoized_property

class AOBasis(object):

    def __init__(
        self,
        *,
        shells : list,
        ):

        self.shells = shells

        if not all(isinstance(_, list) for _ in shells): raise RuntimeError('shells is not list of lists')
        if not all(isinstance(_, AOShell) for _ in self.shells_unrolled): raise RuntimeError('shells is not list of list of AOShell')

    @memoized_property
    def is_pure(self):
        return all(_.is_pure for _ in self.shells_unrolled)

    @memoized_property
    def is_cart(self):
        return all(not _.is_pure for _ in self.shells_unrolled)

    @memoized_property
    def is_mixed(self):
        return (not self.is_pure) and (not self.is_cart)

    @memoized_property
    def natom(self):
        return len(self.shells)

    @memoized_property
    def nshell(self):
        return len(self.shells_unrolled)

    @memoized_property
    def nao(self):
        return sum(_.nao for _ in self.shells_unrolled)

    @memoized_property
    def ncart(self):
        return sum(_.ncart for _ in self.shells_unrolled)

    @memoized_property
    def nprimitive(self):
        return sum(_.nprimitive for _ in self.shells_unrolled)

    @memoized_property
    def max_L(self):
        return max(_.L for _ in self.shells_unrolled) if self.nshell else 0

    @memoized_property
    def shells_unrolled(self):
        return [item for sublist in self.shells for item in sublist]

    # => Indexing <= #

    @memoized_property
    def shell_ao_starts(self):
        if self.nshell == 0: return []
        return [0] + list(np.cumsum([_.nao for _ in self.shells_unrolled]))[:-1]

    @memoized_property
    def shell_cart_starts(self):
        if self.nshell == 0: return []
        return [0] + list(np.cumsum([_.ncart for _ in self.shells_unrolled]))[:-1]

    @memoized_property
    def shell_primitive_starts(self):
        if self.nshell == 0: return []
        return [0] + list(np.cumsum([_.nprimitive for _ in self.shells_unrolled]))[:-1]

    @memoized_property
    def shell_atoms(self):
        return [item for sublist in [[A]*len(shells2) for A, shells2 in enumerate(self.shells)] for item in sublist]

    @memoized_property
    def shell_atom_shells(self):
        return [item for sublist in [list(range(len(shells2))) for shells2 in self.shells] for item in sublist]

    def update_to_pure(self):
        return AOBasis(shells=[[shell.update_to_pure() for shell in shells2] for shells2 in self.shells])

    def update_to_cart(self):
        return AOBasis(shells=[[shell.update_to_cart() for shell in shells2] for shells2 in self.shells])

    def clone(self):
        shells = []
        for shells2 in self.shells:
            shells.append([_.clone() for _ in shells2])
        return AOBasis(shells=shells)

    @staticmethod
    def concatenate(bases):
        shells = []
        for basis in bases:
            shells += basis.clone().shells
        return AOBasis(shells=shells)

    def subset(self, indices):
        return AOBasis(shells=[[shell.clone() for shell in self.shells[A]] for A in indices])

    def atomize(self):
        return [self.subset(indices=[A]) for A in range(self.natom)]

    def __eq__(self, other):
        if not isinstance(other, AOBasis):
            return NotImplemented
        return self.shells_unrolled == other.shells_unrolled

    def __ne__(self, other):
        if not isinstance(other, AOBasis):
            return NotImplemented
        return self.shells_unrolled != other.shells_unrolled

    # => String Details <= #

    def __str__(self):
        return self.string

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

    @property
    def detail_string(self):
        s = ''
        s += '%-5s : %7s %2s %5s %5s %5s %10s %10s %10s %10s %10s\n' % (
            'shell',
            'is_pure',
            'L',
            'nao',
            'ncart',
            'nprim',
            'ao_start',
            'cart_start',
            'prim_start',
            'atom',
            'atom_shell',
            )
        P = 0
        for A in range(self.natom):
            for P2 in range(len(self.shells[A])):
                shell = self.shells[A][P2]
                s += '%-5d : %7s %2d %5d %5d %5d %10d %10d %10d %10d %10d\n' % (
                    P,
                    shell.is_pure,
                    shell.L,
                    shell.nao,
                    shell.ncart,
                    shell.nprimitive,
                    self.shell_ao_starts[P],
                    self.shell_cart_starts[P],
                    self.shell_primitive_starts[P],
                    self.shell_atoms[P],
                    self.shell_atom_shells[P],
                    )
                P += 1
        return s

    # => GBS File AOBasis Parsing <= #

    angular_momentum_table = {
        'S' :  0,
        'P' :  1,
        'D' :  2,
        'F' :  3,
        'G' :  4,
        'H' :  5,
        'I' :  6,
        'K' :  7,
        'L' :  8,
        'M' :  9,
        'N' : 10,
        'O' : 11,
        'Q' : 12,
        'R' : 13,
        'T' : 14,
        'U' : 15,
        'V' : 16,
        'W' : 17,
        'X' : 18,
        'Y' : 19,
        'Z' : 20,
        }

    @staticmethod
    def parse_from_gbs_lines(
        *,
        lines,
        molecule,
        ):

        import re

        # => Cleaning <= #

        # Strip blank lines out
        lines = [_ for _ in lines if len(_.strip())]
        # Strip comment lines out
        re_comment = re.compile(r'\s*!')
        lines = [_ for _ in lines if not re.match(re_comment, _)]
        # Lines must be nonzero
        if len(lines) == 0: raise RuntimeError('GBS lines are blank')

        # => Spherical / Cartesian Tag Line <= #

        mobj = re.match(r'^\s*(spherical|cartesian)\s*$', lines[0])
        if mobj is None:
            raise RuntimeError("First line of GBS file must be 'spherical' or 'cartesian', instead is: %s" % lines[0])

        if mobj.group(1) == 'spherical':
            is_pure = True
        elif mobj.group(1) == 'cartesian':
            is_pure = False
        else:
            raise RuntimeError('Invalid cartesian/spherical label: %s' % mobj.group(1))

        lines = lines[1:]

        # => Atom Block Location <= #

        re_separator = re.compile(r'\s*\*\*\*\*\s*$')
        separator_indices = [k for k, line in enumerate(lines) if re.match(re_separator, line)]
        if len(separator_indices) == 0:
            raise RuntimeError('No **** separators present')
        if separator_indices[-1] + 1 != len(lines):
            raise RuntimeError('Last line must be ****, instead is: %s' % lines[-1])
        lines = lines[:-1]
        separator_indices = separator_indices[:-1]

        if len(lines) == 0: raise RuntimeError('GBS lines are blank')
        if len(separator_indices) == 0: raise RuntimeError('No **** separators present')

        # => Atom IDs and corresponding block index <= #

        re_atom_id = re.compile(r'^\s*(\S+)\s+(\d+)\s*$')
        atom_id_lines = [lines[_ + 1] for _ in separator_indices]
        N_index_map = {}
        for index, atom_id_line in enumerate(atom_id_lines):
            mobj = re.match(re_atom_id, atom_id_line)
            if mobj is None:
                raise RuntimeError('Malformed atom ID line: %s' % (atom_id_line))
            symbol = mobj.group(1)
            atom_index = int(mobj.group(2))
            if atom_index != 0:
                raise RuntimeError('"Symbol 0" is only allowed atom line - multiple basis sets per atom type in GBS files is not supported by this library')
            symbol_upper = symbol.upper()
            if symbol_upper not in PeriodicTable.symbol_upper_to_N_table:
                raise RuntimeError('Unknown atomic in GBS file symbol: %s' % symbol)
            N = PeriodicTable.symbol_upper_to_N_table[symbol_upper]
            if N in N_index_map:
                raise RuntimeError('Duplicate atomic symbol in GBS file: %s' % symbol)
            N_index_map[N] = index

        # => Unique atoms in molecule (will parse only these) <= #

        unique_N = list(set(molecule.N[...]))

        re_shell_type = re.compile(r'^\s*(\S+)\s+(\d+)\s+(\S+)\s*$')
        re_primitive = re.compile(r'^\s*(\S+)\s+(\S+)\s*$')

        N_to_shells_map = {}
        for N in unique_N:
            if N not in N_index_map:
                raise RuntimeError('N not in GBS file: %d' % (N))
            block_index = N_index_map[N]
            index1 = separator_indices[block_index + 0]
            index2 = separator_indices[block_index + 1] if block_index + 1 < len(separator_indices) else len(lines)

            block_lines = lines[index1+2:index2]
            shell_type_indices = [k for k, line in enumerate(block_lines) if re.match(re_shell_type, line)]

            shells = []
            for index in shell_type_indices:
                shell_type_line = block_lines[index]
                mobj = re.match(re_shell_type, shell_type_line)
                am_symbol_upper = mobj.group(1).upper()
                if am_symbol_upper not in AOBasis.angular_momentum_table:
                    raise RuntimeError('Unknown angular momentum symbol: %s' % am_symbol_upper)
                L = AOBasis.angular_momentum_table[am_symbol_upper]
                nprimitive = int(mobj.group(2))
                normalization = float(mobj.group(3))
                exponents = []
                coefficients_raw = []
                for K in range(nprimitive):
                    primitive_line = block_lines[index + 1 + K]
                    # Hack to replace D with E for FORTRAN notation
                    primitive_line = primitive_line.replace('D', 'E')
                    primitive_line = primitive_line.replace('d', 'e')
                    mobj = re.match(re_primitive, primitive_line)
                    exponents += [float(mobj.group(1))]
                    coefficients_raw += [float(mobj.group(2))]
                exponents = np.array(exponents)
                coefficients_raw = np.array(coefficients_raw)
                shells.append(AOShell.build_from_raw_data(
                    is_pure=is_pure,
                    L=L,
                    exponents=exponents,
                    coefficients_raw=coefficients_raw,
                    normalization=normalization,
                    ))
            N_to_shells_map[N] = shells

        # => AOBasis <= #

        shells = []
        for N in molecule.N:
            shells.append(N_to_shells_map[N])

        return AOBasis(
            shells=shells,
            )

    @staticmethod
    def parse_from_gbs_string(
        string,
        *,
        molecule,
        **kwargs):

        return AOBasis.parse_from_gbs_lines(lines=string.split('\n'), molecule=molecule, **kwargs)

    @staticmethod
    def parse_from_gbs_file(
        filename,
        *,
        molecule,
        **kwargs):

        with open(filename, 'r') as fh:
            lines = fh.read().splitlines()

        return AOBasis.parse_from_gbs_lines(
            lines=lines,
            molecule=molecule,
            **kwargs)