def integrate(self, g1, g2, g3, g4):
        self.end_dict = {}

        g5 = gaussian_product(g1, g2)
        g6 = gaussian_product(g3, g4)

        a_1 = g1.exponent
        a_2 = g2.exponent
        a_3 = g3.exponent
        a_4 = g4.exponent
        a_5 = g5.exponent
        a_6 = g6.exponent

        r_1 = g1.coordinates
        r_2 = g2.coordinates
        r_3 = g3.coordinates
        r_4 = g4.coordinates
        r_5 = g5.coordinates
        r_6 = g6.coordinates
        r_12 = coordinate_distance(r_1, r_2)
        r_34 = coordinate_distance(r_3, r_4)
        r_56 = coordinate_distance(r_5, r_6)

        l_1 = g1.integral_exponents
        l_2 = g2.integral_exponents
        l_3 = g3.integral_exponents
        l_4 = g4.integral_exponents
        l_5 = g5.integral_exponents
        l_6 = g6.integral_exponents

        delta = (1/(4*a_5)) + (1/(4*a_6))

        ans = 0
        for l in range(l_5[0] + 1):
            for r in range(int(l/2) + 1):
                for ll in range(l_6[0] + 1):
                    for rr in range(int(ll/2) + 1):
                        for i in range(int((l + ll - 2*r - 2*rr) / 2) + 1):
                            out1 = self.b_function(l, ll, r, rr, i, l_1[0], l_2[0], r_1[0], r_2[0], r_5[0], a_5, l_3[0], l_4[0], r_3[0], r_4[0], r_6[0], a_6)
                            for m in range(l_5[1] + 1):
                                for s in range(int(m/2) + 1):
                                    for mm in range(l_6[1] + 1):
                                        for ss in range(int(mm/2) + 1):
                                            for j in range(int((m + mm - 2*s - 2*ss) / 2) + 1):
                                                out2 = self.b_function(m, mm, s, ss, j, l_1[1], l_2[1], r_1[1], r_2[1], r_5[1], a_5, l_3[1], l_4[1], r_3[1], r_4[1], r_6[1], a_6)
                                                for n in range(l_5[2] + 1):
                                                    for t in range(int(n/2) + 1):
                                                        for nn in range(l_6[2] + 1):
                                                            for tt in range(int(nn/2) + 1):
                                                                for k in range(int((n + nn - 2*t - 2*tt) / 2) + 1):
                                                                    out3 = self.b_function(n, nn, t, tt, k, l_1[2], l_2[2], r_1[2], r_2[2], r_5[2], a_5, l_3[2], l_4[2], r_3[2], r_4[2], r_6[2], a_6)
                                                                    v = l + ll + m + mm + n + nn - 2*(r + rr + s + ss + t + tt) - (i + j + k)
                                                                    if v in self.end_dict:
                                                                        out4 = self.end_dict[v]
                                                                    else:
                                                                        out4 = boys_function(v, (r_56**2 / (4 * delta)))
                                                                        self.end_dict[v] = out4
                                                                    ans += out1 * out2 * out3 * out4
        ans *= self.gaussian_product_factor(a_1, a_2, a_3, a_4, a_5, a_6, r_12, r_34)
        return ans
    def integrate(self, g1, g2, g3, g4):
        l_1 = g1.integral_exponents
        l_2 = g2.integral_exponents
        l_3 = g3.integral_exponents
        l_4 = g4.integral_exponents
        l_total = sum(l_1) + sum(l_2) + sum(l_3) + sum(l_4)

        a_1 = g1.exponent
        a_2 = g2.exponent
        a_3 = g3.exponent
        a_4 = g4.exponent
        a_5 = a_1 + a_2
        a_6 = a_3 + a_4
        self.a_7 = (a_5 * a_6) / (a_5 + a_6)

        r_1 = g1.coordinates
        r_2 = g2.coordinates
        r_3 = g3.coordinates
        r_4 = g4.coordinates
        r_5 = gaussian_product_coordinate(a_1, r_1, a_2, r_2)
        r_6 = gaussian_product_coordinate(a_3, r_3, a_4, r_4)
        self.r_7 = gaussian_product_coordinate(a_5, r_5, a_6, r_6)

        r_12 = coordinate_distance(r_1, r_2)
        r_34 = coordinate_distance(r_3, r_4)
        r_56 = coordinate_distance(r_5, r_6)

        boys_x = (a_5 * a_6 * r_56**2) / (a_5 + a_6)
        boys_out1 = (2 * pi**(5/2)) / (a_5 * a_6 * sqrt(a_5 + a_6))
        boys_out2 = exp(((- a_1 * a_2 * r_12**2) / a_5) - ((a_3 * a_4 * r_34**2) / a_6))
        boys_out3 = boys_function(l_total, boys_x)

        self.end_dict = {l_total: boys_out1 * boys_out2 * boys_out3}

        while l_total >= 1:
            boys_out3 = boys_function_recursion(l_total, boys_x, boys_out3)
            l_total -= 1
            self.end_dict[l_total] = boys_out1 * boys_out2 * boys_out3

        if sum(l_1) >= sum(l_2) and sum(l_3) >= sum(l_4):
            return self.hgp_begin_horizontal(g1, g2, g3, g4)
        elif sum(l_1) >= sum(l_2):
            return self.hgp_begin_horizontal(g1, g2, g4, g3)
        elif sum(l_3) >= sum(l_4):
            return self.hgp_begin_horizontal(g2, g1, g3, g4)
        else:
            return self.hgp_begin_horizontal(g2, g1, g4, g3)
    def check_basis_functions(self, basis_i, basis_x):
        i, j, k = basis_i.integral_exponents
        x, y, z = basis_x.integral_exponents
        i, j, k = abs(i), abs(j), abs(k)

        if coordinate_distance(basis_i.coordinates, basis_x.coordinates) <= 1e-3 and i == x and j == y and k == z \
        and self.check_primitives(basis_i, basis_x):
            return True
        else:
            return False
def nuclear_attraction(gaussian_1, gaussian_2, nuclei):
    a_1 = gaussian_1.exponent
    a_2 = gaussian_2.exponent
    l_1 = gaussian_1.integral_exponents
    l_2 = gaussian_2.integral_exponents

    r_a = gaussian_1.coordinates
    r_b = gaussian_2.coordinates
    r_c = nuclei.coordinates
    r_p = gaussian_product_coordinate(a_1, r_a, a_2, r_b)

    r_ab = coordinate_distance(r_a, r_b)
    r_pc = coordinate_distance(r_p, r_c)

    r_p_a = vector_minus(r_p, r_a)
    r_p_b = vector_minus(r_p, r_b)
    r_p_c = vector_minus(r_p, r_c)

    g = a_1 + a_2

    ans = 0
    for l in range(l_1[0] + l_2[0] + 1):
        for r in range(int(l/2) + 1):
            for i in range(int((l - 2*r) / 2) + 1):
                out1 = a_function(l, r, i, l_1[0], l_2[0], r_p_a[0], r_p_b[0], r_p_c[0], g)
                for m in range(l_1[1] + l_2[1] + 1):
                    for s in range(int(m/2) + 1):
                        for j in range(int((m - 2*s) / 2) + 1):
                            out2 = a_function(m, s, j, l_1[1], l_2[1], r_p_a[1], r_p_b[1], r_p_c[1], g)
                            for n in range(l_1[2] + l_2[2] + 1):
                                for t in range(int(n/2) + 1):
                                    for k in range(int((n - 2*t) / 2) + 1):
                                        out3 = a_function(n, t, k, l_1[2], l_2[2], r_p_a[2], r_p_b[2], r_p_c[2], g)
                                        v = (l + m + n) - 2*(r + s + t) - (i + j + k)
                                        out4 = boys_function(v, g * r_pc**2)
                                        out5 = out1 * out2 * out3 * out4
                                        ans += out5
    ans *= ((2 * pi) / g) * exp(- (a_1 * a_2 * r_ab**2) / g)
    return ans
 def check_symmetry_operation(self, nuclei_array, symmetry):
     nuclei_array_copy = []
     for nuclei in nuclei_array:
         coordinates = symmetry.operate(nuclei.coordinates)
         nuclei_copy = Nuclei(nuclei.element, nuclei.charge, nuclei.mass, coordinates)
         nuclei_array_copy.append(nuclei_copy)
     for nuclei_i in nuclei_array:
         for k, nuclei_k in enumerate(nuclei_array_copy):
             if coordinate_distance(nuclei_i.coordinates, nuclei_k.coordinates) <= self.error \
             and (nuclei_i.charge - nuclei_k.charge) == 0.0:
                 break
             if k == len(nuclei_array_copy) - 1:
                 return False
     return True
def coulombs_law(nuc1, nuc2):
    """Computes the nuclear-nuclear repulsion energy for two nuclei.

    Parameters
    ----------
    nuc1 : Nuclei
    nuc2 : Nuclei

    Returns
    -------
    ans : float

    """
    r_12 = coordinate_distance(nuc1.coordinates, nuc2.coordinates)
    ans = (nuc1.charge * nuc2.charge) / r_12
    return ans
def orbital_overlap(gaussian_1, gaussian_2):
    a_1 = gaussian_1.exponent
    a_2 = gaussian_2.exponent
    l_1 = gaussian_1.integral_exponents
    l_2 = gaussian_2.integral_exponents

    r_a = gaussian_1.coordinates
    r_b = gaussian_2.coordinates
    r_ab = coordinate_distance(r_a, r_b)

    r_p = gaussian_product_coordinate(a_1, r_a, a_2, r_b)
    r_p_a = vector_minus(r_p, r_a)
    r_p_b = vector_minus(r_p, r_b)

    g = a_1 + a_2

    s_x = s_function(l_1[0], l_2[0], r_p_a[0], r_p_b[0], g)
    s_y = s_function(l_1[1], l_2[1], r_p_a[1], r_p_b[1], g)
    s_z = s_function(l_1[2], l_2[2], r_p_a[2], r_p_b[2], g)
    s_ij = (pi / g)**(3/2) * exp(- a_1 * a_2 * r_ab**2 / g) * s_x * s_y * s_z
    return s_ij