mqc_gmbe_utils.f90 Source File

GMBE (Generalized Many-Body Expansion) utilities for overlapping fragments


This file depends on

sourcefile~~mqc_gmbe_utils.f90~~EfferentGraph sourcefile~mqc_gmbe_utils.f90 mqc_gmbe_utils.f90 sourcefile~mqc_combinatorics.f90 mqc_combinatorics.f90 sourcefile~mqc_gmbe_utils.f90->sourcefile~mqc_combinatorics.f90 sourcefile~mqc_error.f90 mqc_error.f90 sourcefile~mqc_gmbe_utils.f90->sourcefile~mqc_error.f90 sourcefile~mqc_physical_fragment.f90 mqc_physical_fragment.f90 sourcefile~mqc_gmbe_utils.f90->sourcefile~mqc_physical_fragment.f90 sourcefile~mqc_result_types.f90 mqc_result_types.f90 sourcefile~mqc_gmbe_utils.f90->sourcefile~mqc_result_types.f90 sourcefile~mqc_combinatorics.f90->sourcefile~mqc_physical_fragment.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_error.f90 sourcefile~mqc_cgto.f90 mqc_cgto.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_cgto.f90 sourcefile~mqc_elements.f90 mqc_elements.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_elements.f90 sourcefile~mqc_geometry.f90 mqc_geometry.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_geometry.f90 sourcefile~mqc_physical_constants.f90 mqc_physical_constants.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_physical_constants.f90 sourcefile~mqc_xyz_reader.f90 mqc_xyz_reader.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_xyz_reader.f90 sourcefile~mqc_result_types.f90->sourcefile~mqc_error.f90 sourcefile~mqc_xyz_reader.f90->sourcefile~mqc_error.f90 sourcefile~mqc_xyz_reader.f90->sourcefile~mqc_geometry.f90

Files dependent on this one

sourcefile~~mqc_gmbe_utils.f90~~AfferentGraph sourcefile~mqc_gmbe_utils.f90 mqc_gmbe_utils.f90 sourcefile~mqc_frag_utils.f90 mqc_frag_utils.f90 sourcefile~mqc_frag_utils.f90->sourcefile~mqc_gmbe_utils.f90 sourcefile~mqc_mbe.f90 mqc_mbe.f90 sourcefile~mqc_mbe.f90->sourcefile~mqc_gmbe_utils.f90 sourcefile~mqc_mbe.f90->sourcefile~mqc_frag_utils.f90 sourcefile~mqc_driver.f90 mqc_driver.f90 sourcefile~mqc_driver.f90->sourcefile~mqc_frag_utils.f90 sourcefile~mqc_driver.f90->sourcefile~mqc_mbe.f90 sourcefile~mqc_mbe_fragment_distribution_scheme.f90 mqc_mbe_fragment_distribution_scheme.F90 sourcefile~mqc_driver.f90->sourcefile~mqc_mbe_fragment_distribution_scheme.f90 sourcefile~mqc_many_body_expansion.f90 mqc_many_body_expansion.f90 sourcefile~mqc_driver.f90->sourcefile~mqc_many_body_expansion.f90 sourcefile~mqc_mbe_fragment_distribution_scheme.f90->sourcefile~mqc_mbe.f90 sourcefile~main.f90 main.f90 sourcefile~main.f90->sourcefile~mqc_driver.f90 sourcefile~mqc_calculation_interface.f90 mqc_calculation_interface.f90 sourcefile~mqc_calculation_interface.f90->sourcefile~mqc_driver.f90 sourcefile~mqc_gmbe_fragment_distribution_scheme.f90 mqc_gmbe_fragment_distribution_scheme.f90 sourcefile~mqc_gmbe_fragment_distribution_scheme.f90->sourcefile~mqc_mbe_fragment_distribution_scheme.f90 sourcefile~mqc_many_body_expansion.f90->sourcefile~mqc_mbe_fragment_distribution_scheme.f90 sourcefile~mqc_many_body_expansion.f90->sourcefile~mqc_gmbe_fragment_distribution_scheme.f90 sourcefile~mqc_mbe_fragment_distribution_scheme_hessian.f90 mqc_mbe_fragment_distribution_scheme_hessian.F90 sourcefile~mqc_mbe_fragment_distribution_scheme_hessian.f90->sourcefile~mqc_mbe_fragment_distribution_scheme.f90 sourcefile~mqc_mbe_mpi_fragment_distribution_scheme.f90 mqc_mbe_mpi_fragment_distribution_scheme.F90 sourcefile~mqc_mbe_mpi_fragment_distribution_scheme.f90->sourcefile~mqc_mbe_fragment_distribution_scheme.f90 sourcefile~mqc_serial_fragment_processor.f90 mqc_serial_fragment_processor.f90 sourcefile~mqc_serial_fragment_processor.f90->sourcefile~mqc_mbe_fragment_distribution_scheme.f90 sourcefile~mqc_unfragmented_workflow.f90 mqc_unfragmented_workflow.f90 sourcefile~mqc_unfragmented_workflow.f90->sourcefile~mqc_mbe_fragment_distribution_scheme.f90

Source Code

!! GMBE (Generalized Many-Body Expansion) utilities for overlapping fragments
module mqc_gmbe_utils
   !! Provides functions for computing fragment intersections, generating k-way
   !! intersections, and enumerating PIE (Principle of Inclusion-Exclusion) terms
   !! for GMBE calculations with overlapping molecular fragments.
   use pic_types, only: default_int, int32, int64, dp
   use pic_logger, only: logger => global_logger, info_level
   use pic_io, only: to_char
   use mqc_combinatorics, only: next_combination, next_combination_init
   use mqc_error, only: error_t, ERROR_VALIDATION
   use mqc_physical_fragment, only: system_geometry_t
   use mqc_result_types, only: calculation_result_t
   implicit none
   private

   public :: find_fragment_intersection      !! Find shared atoms between two fragments
   public :: generate_intersections          !! Generate all k-way intersections for GMBE
   public :: compute_polymer_atoms           !! Compute atom list for polymer (union of fragments)
   public :: generate_polymer_intersections  !! Generate intersections for polymers
   public :: gmbe_enumerate_pie_terms        !! DFS-based PIE coefficient enumeration
   public :: print_gmbe_energy_breakdown     !! Print GMBE energy breakdown
   public :: print_gmbe_gradient_info        !! Print GMBE gradient information
   public :: print_gmbe_intersection_debug   !! Print debug information about GMBE intersections

contains

   function find_fragment_intersection(frag1_atoms, n1, frag2_atoms, n2, &
                                       intersection, n_intersect) result(has_intersection)
      !! Find shared atoms between two fragments (for GMBE with overlapping fragments)
      !!
      !! This function identifies atoms that appear in both fragments, which is essential
      !! for computing intersection-corrected energies in GMBE.
      !!
      !! Algorithm: O(n1 * n2) brute-force comparison
      !! - Loop through all atoms in fragment 1
      !! - For each atom, check if it appears in fragment 2
      !! - Collect all shared atoms
      !!
      !! Returns:
      !!   .true. if fragments share at least one atom, .false. otherwise
      !!
      !! Output:
      !!   intersection - allocatable array containing shared atom indices
      !!   n_intersect - number of shared atoms
      integer, intent(in) :: frag1_atoms(:)  !! Atom indices in fragment 1 (0-indexed)
      integer, intent(in) :: n1              !! Number of atoms in fragment 1
      integer, intent(in) :: frag2_atoms(:)  !! Atom indices in fragment 2 (0-indexed)
      integer, intent(in) :: n2              !! Number of atoms in fragment 2
      integer, allocatable, intent(out) :: intersection(:)  !! Shared atom indices
      integer, intent(out) :: n_intersect    !! Number of shared atoms
      logical :: has_intersection

      integer :: i, j
      integer, allocatable :: temp_intersection(:)
      integer :: temp_count

      ! Allocate temporary array (max possible size is min(n1, n2))
      allocate (temp_intersection(min(n1, n2)))
      temp_count = 0

      ! Find all shared atoms
      do i = 1, n1
         do j = 1, n2
            if (frag1_atoms(i) == frag2_atoms(j)) then
               ! Found a shared atom
               temp_count = temp_count + 1
               temp_intersection(temp_count) = frag1_atoms(i)
               exit  ! Move to next atom in frag1
            end if
         end do
      end do

      ! Set output
      n_intersect = temp_count
      has_intersection = (temp_count > 0)

      ! Allocate and copy result if intersection exists
      if (has_intersection) then
         allocate (intersection(n_intersect))
         intersection = temp_intersection(1:n_intersect)
      end if

      deallocate (temp_intersection)

   end function find_fragment_intersection

   subroutine generate_intersections(sys_geom, monomers, polymers, n_monomers, max_intersection_level, &
                                     intersections, intersection_sets, intersection_levels, n_intersections)
      !! Generate all k-way intersections for k=2 to min(max_intersection_level, n_monomers)
      !!
      !! For a system with overlapping fragments, this computes k-way intersections
      !! following the inclusion-exclusion principle for GMBE.
      !! The max_intersection_level parameter controls the maximum depth to avoid combinatorial explosion.
      !!
      !! Algorithm:
      !! - For each k from 2 to min(max_intersection_level, n_monomers):
      !!   - Generate all C(n_monomers, k) combinations
      !!   - For each combination, compute intersection of all k fragments
      !!   - Store non-empty intersections with their level k
      use mqc_physical_fragment, only: system_geometry_t
      type(system_geometry_t), intent(in) :: sys_geom
      integer, intent(in) :: monomers(:)           !! Monomer indices
      integer, intent(inout) :: polymers(:, :)     !! Output: monomers stored here
      integer, intent(in) :: n_monomers            !! Number of monomers
      integer, intent(in) :: max_intersection_level  !! Maximum k-way intersection depth
      integer, allocatable, intent(out) :: intersections(:, :)  !! Intersection atom lists
      integer, allocatable, intent(out) :: intersection_sets(:, :)  !! Which k-tuple created each intersection
      integer, allocatable, intent(out) :: intersection_levels(:)  !! Level (k) of each intersection
      integer, intent(out) :: n_intersections      !! Number of intersections found

      ! Temporaries for storing intersections
      integer, allocatable :: temp_intersections(:, :)
      integer, allocatable :: temp_sets(:, :)
      integer, allocatable :: temp_levels(:)
      integer, allocatable :: temp_intersection(:)
      integer, allocatable :: current_intersection(:)
      integer :: temp_n_intersect, current_n_intersect
      logical :: has_intersection
      integer :: k, intersection_count, max_atoms, max_intersections, max_k_level
      integer :: i, idx
      integer, allocatable :: combination(:)

      ! Store monomers in polymers array
      polymers(1:n_monomers, 1) = monomers(1:n_monomers)

      if (n_monomers < 2) then
         n_intersections = 0
         return
      end if

      ! Count maximum possible intersections: sum of C(n,k) for k=2 to n
      ! For small n, this is 2^n - n - 1
      max_intersections = 2**n_monomers - n_monomers - 1

      ! Find maximum atoms in any fragment for allocation
      max_atoms = maxval(sys_geom%fragment_sizes(1:n_monomers))

      ! Allocate temporary arrays
      allocate (temp_intersections(max_atoms, max_intersections))
      allocate (temp_sets(n_monomers, max_intersections))
      allocate (temp_levels(max_intersections))
      temp_intersections = 0
      temp_sets = 0
      intersection_count = 0

      ! Determine actual maximum intersection level to use
      max_k_level = min(max_intersection_level, n_monomers)

      if (max_k_level < n_monomers) then
         call logger%info("Generating k-way intersections up to k="//to_char(max_k_level)// &
                          " (limited by max_intersection_level)")
      else
         call logger%info("Generating all k-way intersections for GMBE (inclusion-exclusion principle)")
      end if

      ! Loop over intersection levels k from 2 to max_k_level
      do k = 2, max_k_level
         ! Generate all C(n_monomers, k) combinations
         allocate (combination(k))
         call generate_k_way_intersections_for_level(sys_geom, monomers, n_monomers, k, &
                                                     combination, max_atoms, &
                                                     temp_intersections, temp_sets, temp_levels, intersection_count)
         deallocate (combination)
      end do

      n_intersections = intersection_count

      ! Allocate output arrays
      if (n_intersections > 0) then
         allocate (intersections(max_atoms, n_intersections))
         allocate (intersection_sets(n_monomers, n_intersections))
         allocate (intersection_levels(n_intersections))
         intersections = temp_intersections(1:max_atoms, 1:n_intersections)
         intersection_sets = temp_sets(1:n_monomers, 1:n_intersections)
         intersection_levels = temp_levels(1:n_intersections)

         call logger%info("Generated "//to_char(n_intersections)//" total intersections:")
         do k = 2, max_k_level
            idx = count(intersection_levels == k)
            if (idx > 0) then
               call logger%info("  "//to_char(idx)//" intersections at level "//to_char(k))
            end if
         end do
      else
         call logger%info("No intersections found (fragments are non-overlapping)")
      end if

      deallocate (temp_intersections, temp_sets, temp_levels)

   end subroutine generate_intersections

   recursive subroutine generate_k_way_intersections_for_level(sys_geom, monomers, n_monomers, k, &
                                                               combination, max_atoms, &
                                                           temp_intersections, temp_sets, temp_levels, intersection_count)
      !! Helper to generate all k-way intersections at a specific level k
      use mqc_physical_fragment, only: system_geometry_t
      type(system_geometry_t), intent(in) :: sys_geom
      integer, intent(in) :: monomers(:), n_monomers, k, max_atoms
      integer, intent(inout) :: combination(:)
      integer, intent(inout) :: temp_intersections(:, :), temp_sets(:, :), temp_levels(:)
      integer, intent(inout) :: intersection_count

      integer, allocatable :: current_intersection(:), temp_intersection(:)
      integer :: current_n_intersect, temp_n_intersect
      integer :: i, mono_idx, frag_size
      logical :: has_intersection

      ! Generate combinations using an iterative approach
      call next_combination_init(combination, k)

      do
         ! Compute intersection of all fragments in this combination
         has_intersection = .false.

         ! Start with the first fragment in the combination
         mono_idx = monomers(combination(1))
         frag_size = sys_geom%fragment_sizes(mono_idx)
         allocate (current_intersection(frag_size))
         current_intersection = sys_geom%fragment_atoms(1:frag_size, mono_idx)
         current_n_intersect = frag_size

         ! Intersect with each subsequent fragment
         do i = 2, k
            mono_idx = monomers(combination(i))
            frag_size = sys_geom%fragment_sizes(mono_idx)

            has_intersection = find_fragment_intersection( &
                               current_intersection, current_n_intersect, &
                               sys_geom%fragment_atoms(1:frag_size, mono_idx), frag_size, &
                               temp_intersection, temp_n_intersect)

            if (.not. has_intersection) then
               ! Intersection is empty, break early
               deallocate (current_intersection)
               if (allocated(temp_intersection)) deallocate (temp_intersection)
               exit
            end if

            ! Replace current_intersection with the new intersection
            deallocate (current_intersection)
            allocate (current_intersection(temp_n_intersect))
            current_intersection = temp_intersection
            current_n_intersect = temp_n_intersect
            deallocate (temp_intersection)
         end do

         ! If we have a non-empty intersection, store it
         if (has_intersection .and. allocated(current_intersection)) then
            intersection_count = intersection_count + 1
            temp_intersections(1:current_n_intersect, intersection_count) = current_intersection
            temp_sets(1:k, intersection_count) = monomers(combination)
            temp_levels(intersection_count) = k
            deallocate (current_intersection)
         end if

         ! Get next combination
         if (.not. next_combination(combination, k, n_monomers)) exit
      end do

   end subroutine generate_k_way_intersections_for_level

   pure subroutine compute_polymer_atoms(sys_geom, polymer, polymer_size, atom_list, n_atoms)
      !! Compute the atom list for a polymer (union of atoms from base fragments)
      !! polymer(:) contains base fragment indices (1-based)
      use mqc_physical_fragment, only: system_geometry_t
      type(system_geometry_t), intent(in) :: sys_geom
      integer, intent(in) :: polymer(:)  !! Base fragment indices in this polymer
      integer, intent(in) :: polymer_size  !! Number of base fragments in polymer
      integer, allocatable, intent(out) :: atom_list(:)  !! Unique atoms in this polymer
      integer, intent(out) :: n_atoms  !! Number of unique atoms

      integer, allocatable :: temp_atoms(:)
      integer :: i, j, frag_idx, frag_size, temp_count
      logical :: already_present

      ! Allocate temporary array (worst case: all atoms from all fragments)
      allocate (temp_atoms(sys_geom%total_atoms))
      temp_count = 0

      ! Loop through each base fragment in the polymer
      do i = 1, polymer_size
         frag_idx = polymer(i)
         if (frag_idx == 0) exit  ! Padding zeros

         frag_size = sys_geom%fragment_sizes(frag_idx)

         ! Add each atom from this fragment (avoid duplicates)
         do j = 1, frag_size
            already_present = .false.

            ! Check if this atom is already in our list
            block
               integer :: k, current_atom
               current_atom = sys_geom%fragment_atoms(j, frag_idx)
               do k = 1, temp_count
                  if (temp_atoms(k) == current_atom) then
                     already_present = .true.
                     exit
                  end if
               end do
            end block

            ! Add if not already present
            if (.not. already_present) then
               temp_count = temp_count + 1
               temp_atoms(temp_count) = sys_geom%fragment_atoms(j, frag_idx)
            end if
         end do
      end do

      ! Copy to output array
      n_atoms = temp_count
      allocate (atom_list(n_atoms))
      atom_list = temp_atoms(1:n_atoms)

      deallocate (temp_atoms)
   end subroutine compute_polymer_atoms

   subroutine generate_polymer_intersections(sys_geom, polymers, n_polymers, max_level, &
                                             intersections, intersection_sets, intersection_levels, n_intersections)
      !! Generate all k-way intersections for polymers at any level (GMBE-N)
      !! This works with dynamically generated polymers, not just base fragments
      use mqc_physical_fragment, only: system_geometry_t
      use pic_logger, only: logger => global_logger
      use pic_io, only: to_char
      type(system_geometry_t), intent(in) :: sys_geom
      integer, intent(in) :: polymers(:, :)  !! Polymer definitions (n_polymers, max_level)
      integer, intent(in) :: n_polymers, max_level
      integer, allocatable, intent(out) :: intersections(:, :)
      integer, allocatable, intent(out) :: intersection_sets(:, :)
      integer, allocatable, intent(out) :: intersection_levels(:)
      integer, intent(out) :: n_intersections

      integer, allocatable :: polymer_atoms(:, :)  !! Atom lists for each polymer
      integer, allocatable :: polymer_n_atoms(:)   !! Number of atoms in each polymer
      integer :: max_atoms_per_polymer
      integer :: i, polymer_size, max_intersection_level

      call logger%info("Computing atom compositions for "//to_char(n_polymers)//" polymers...")

      ! First, compute atom list for each polymer
      ! Find max atoms needed
      max_atoms_per_polymer = 0
      do i = 1, n_polymers
         polymer_size = count(polymers(i, :) > 0)
         ! Worst case: all atoms from all fragments in this polymer
         max_atoms_per_polymer = max(max_atoms_per_polymer, polymer_size*maxval(sys_geom%fragment_sizes))
      end do

      allocate (polymer_atoms(max_atoms_per_polymer, n_polymers))
      allocate (polymer_n_atoms(n_polymers))
      polymer_atoms = 0

      ! Compute atoms for each polymer
      do i = 1, n_polymers
         polymer_size = count(polymers(i, :) > 0)
         block
            integer, allocatable :: atom_list(:)
            integer :: n_atoms
            call compute_polymer_atoms(sys_geom, polymers(i, 1:polymer_size), polymer_size, atom_list, n_atoms)
            polymer_n_atoms(i) = n_atoms
            polymer_atoms(1:n_atoms, i) = atom_list
            deallocate (atom_list)
         end block
      end do

      call logger%info("Finding intersections between polymers...")

      ! For GMBE(N), limit intersections to level N+1 to prevent combinatorial explosion
      ! GMBE(2): dimers → 3-way intersections max
      ! GMBE(3): trimers → 4-way intersections max
      max_intersection_level = max_level + 1
      call logger%info("Limiting intersections to level "//to_char(max_intersection_level)// &
                       " (polymer level "//to_char(max_level)//" + 1)")

      ! Now generate intersections between these polymer atom sets
      call generate_intersections_from_atom_lists(polymer_atoms, polymer_n_atoms, n_polymers, &
                                                  max_intersection_level, &
                                                  intersections, intersection_sets, intersection_levels, n_intersections)

      deallocate (polymer_atoms, polymer_n_atoms)
   end subroutine generate_polymer_intersections

   subroutine generate_intersections_from_atom_lists(atom_lists, n_atoms_list, n_sets, max_k_level, &
                                                   intersections, intersection_sets, intersection_levels, n_intersections)
      !! Generate k-way intersections from arbitrary atom lists (not tied to sys_geom)
      !! max_k_level limits the maximum intersection order to prevent combinatorial explosion
      use pic_logger, only: logger => global_logger
      use pic_io, only: to_char
      integer, intent(in) :: atom_lists(:, :)  !! (max_atoms, n_sets)
      integer, intent(in) :: n_atoms_list(:)   !! Number of atoms in each set
      integer, intent(in) :: n_sets            !! Number of sets (polymers)
      integer, intent(in) :: max_k_level       !! Maximum intersection level to compute
      integer, allocatable, intent(out) :: intersections(:, :)
      integer, allocatable, intent(out) :: intersection_sets(:, :)
      integer, allocatable, intent(out) :: intersection_levels(:)
      integer, intent(out) :: n_intersections

      integer :: max_intersections, max_atoms
      integer, allocatable :: temp_intersections(:, :), temp_sets(:, :), temp_levels(:)
      integer :: intersection_count, k, idx, actual_max_k
      integer, allocatable :: combination(:)

      if (n_sets < 2) then
         n_intersections = 0
         return
      end if

      ! Limit k-way intersections to min(max_k_level, n_sets)
      actual_max_k = min(max_k_level, n_sets)

      max_intersections = 2**n_sets - n_sets - 1
      max_atoms = maxval(n_atoms_list)

      allocate (temp_intersections(max_atoms, max_intersections))
      allocate (temp_sets(n_sets, max_intersections))
      allocate (temp_levels(max_intersections))
      temp_intersections = 0
      temp_sets = 0
      intersection_count = 0

      call logger%info("Generating k-way intersections (k=2 to "//to_char(actual_max_k)//")")

      ! Loop over intersection levels k from 2 to actual_max_k
      do k = 2, actual_max_k
         allocate (combination(k))
         call generate_k_way_intersections_from_lists(atom_lists, n_atoms_list, n_sets, k, &
                                                      combination, max_atoms, &
                                                      temp_intersections, temp_sets, temp_levels, intersection_count)
         deallocate (combination)
      end do

      n_intersections = intersection_count

      ! Allocate output arrays
      if (n_intersections > 0) then
         allocate (intersections(max_atoms, n_intersections))
         allocate (intersection_sets(n_sets, n_intersections))
         allocate (intersection_levels(n_intersections))
         intersections = temp_intersections(1:max_atoms, 1:n_intersections)
         intersection_sets = temp_sets(1:n_sets, 1:n_intersections)
         intersection_levels = temp_levels(1:n_intersections)

         call logger%info("Generated "//to_char(n_intersections)//" total intersections:")
         do k = 2, actual_max_k
            idx = count(intersection_levels == k)
            if (idx > 0) then
               call logger%info("  "//to_char(idx)//" intersections at level "//to_char(k))
            end if
         end do
      else
         allocate (intersections(1, 0))
         allocate (intersection_sets(1, 0))
         allocate (intersection_levels(0))
         call logger%info("No intersections found")
      end if

      deallocate (temp_intersections, temp_sets, temp_levels)
   end subroutine generate_intersections_from_atom_lists

   subroutine generate_k_way_intersections_from_lists(atom_lists, n_atoms_list, n_sets, k, combination, max_atoms, &
                                                      temp_intersections, temp_sets, temp_levels, intersection_count)
      !! Generate all k-way intersections from atom lists
      integer, intent(in) :: atom_lists(:, :), n_atoms_list(:), n_sets, k, max_atoms
      integer, intent(inout) :: combination(:)
      integer, intent(inout) :: temp_intersections(:, :), temp_sets(:, :), temp_levels(:)
      integer, intent(inout) :: intersection_count

      integer, allocatable :: current_intersection(:), temp_intersection(:)
      integer :: current_n_intersect, temp_n_intersect
      integer :: i, j, set_idx
      logical :: has_intersection

      call next_combination_init(combination, k)

      do
         ! Compute intersection of all sets in this combination
         has_intersection = .false.

         ! Start with the first set in the combination
         set_idx = combination(1)
         allocate (current_intersection(n_atoms_list(set_idx)))
         current_intersection = atom_lists(1:n_atoms_list(set_idx), set_idx)
         current_n_intersect = n_atoms_list(set_idx)

         ! Intersect with each subsequent set
         do i = 2, k
            set_idx = combination(i)
            allocate (temp_intersection(current_n_intersect))

            ! Find intersection
            temp_n_intersect = 0
            do j = 1, current_n_intersect
               if (any(atom_lists(1:n_atoms_list(set_idx), set_idx) == current_intersection(j))) then
                  temp_n_intersect = temp_n_intersect + 1
                  temp_intersection(temp_n_intersect) = current_intersection(j)
               end if
            end do

            ! Update current intersection
            deallocate (current_intersection)
            if (temp_n_intersect > 0) then
               allocate (current_intersection(temp_n_intersect))
               current_intersection = temp_intersection(1:temp_n_intersect)
               current_n_intersect = temp_n_intersect
               has_intersection = .true.
            else
               has_intersection = .false.
            end if
            deallocate (temp_intersection)

            if (.not. has_intersection) exit
         end do

         ! Store if we found an intersection
         if (has_intersection .and. current_n_intersect > 0) then
            intersection_count = intersection_count + 1
            temp_intersections(1:current_n_intersect, intersection_count) = current_intersection(1:current_n_intersect)
            temp_sets(1:k, intersection_count) = combination(1:k)
            temp_levels(intersection_count) = k
         end if

         if (allocated(current_intersection)) deallocate (current_intersection)

         ! Get next combination
         if (.not. next_combination(combination, k, n_sets)) exit
      end do
   end subroutine generate_k_way_intersections_from_lists

   subroutine gmbe_enumerate_pie_terms(sys_geom, primaries, n_primaries, polymer_level, max_k_level, &
                                       pie_atom_sets, pie_coefficients, n_pie_terms, error, initial_max_terms)
      !! Enumerate all unique intersections via DFS and accumulate PIE coefficients
      !! This implements the GMBE(N) algorithm with inclusion-exclusion principle
      !!
      !! Algorithm:
      !! 1. For each primary i, start DFS with clique=[i]
      !! 2. Recursively grow cliques by adding overlapping primaries
      !! 3. For each clique of size k, compute intersection and add PIE coefficient:
      !!    coefficient = (+1) if k odd, (-1) if k even
      !! 4. Accumulate coefficients for each unique atom set
      use mqc_physical_fragment, only: system_geometry_t

      type(system_geometry_t), intent(in) :: sys_geom
      integer, intent(in) :: primaries(:, :)   !! Primary polymers (n_primaries, polymer_level)
      integer, intent(in) :: n_primaries       !! Number of primary polymers
      integer, intent(in) :: polymer_level     !! Level of primaries (1=monomers, 2=dimers, etc.)
      integer, intent(in) :: max_k_level       !! Maximum clique size (intersection depth limit)
      integer, allocatable, intent(out) :: pie_atom_sets(:, :)  !! Unique atom sets (max_atoms, n_terms)
      integer, allocatable, intent(out) :: pie_coefficients(:)  !! PIE coefficient for each term
      integer(int64), intent(out) :: n_pie_terms      !! Number of unique PIE terms
      type(error_t), intent(out) :: error             !! Error status
      integer(int64), intent(in), optional :: initial_max_terms  !! Initial PIE storage capacity

      ! Temporary storage for PIE terms (allocate generously)
      integer(int64), parameter :: INITIAL_MAX_PIE_TERMS = 100000_int64  ! Adjust if needed
      integer(int64) :: max_terms
      integer(default_int) :: max_terms_i
      integer, allocatable :: temp_atom_sets(:, :)
      integer, allocatable :: temp_coefficients(:)
      integer, allocatable :: primary_atoms(:, :)    !! Precomputed atom lists for each primary
      integer, allocatable :: primary_n_atoms(:)     !! Atom counts for each primary
      integer, allocatable :: clique(:)              !! Current clique being built
      integer, allocatable :: current_atoms(:)       !! Current intersection atoms
      integer, allocatable :: candidates(:)          !! Candidate primaries to add
      integer :: i, j, max_atoms, n_candidates

      call logger%info("Enumerating GMBE PIE terms via DFS...")

      ! Find maximum atoms in any fragment
      max_atoms = sys_geom%total_atoms

      ! Allocate temporary storage
      if (present(initial_max_terms)) then
         max_terms = initial_max_terms
      else
         max_terms = INITIAL_MAX_PIE_TERMS
      end if
      if (max_terms < 1_int64) then
         call error%set(ERROR_VALIDATION, "Initial PIE term capacity must be positive")
         return
      end if
      if (max_terms > int(huge(0_default_int), int64)) then
         call error%set(ERROR_VALIDATION, "Initial PIE term capacity exceeds default integer limit")
         return
      end if
      max_terms_i = int(max_terms, default_int)

      allocate (temp_atom_sets(max_atoms, max_terms_i))
      allocate (temp_coefficients(max_terms_i))
      temp_atom_sets = -1
      temp_coefficients = 0
      n_pie_terms = 0_int64

      ! Precompute atom lists for all primaries
      allocate (primary_atoms(max_atoms, n_primaries))
      allocate (primary_n_atoms(n_primaries))
      primary_atoms = -1

      do i = 1, n_primaries
         block
            integer, allocatable :: atom_list(:)
            integer :: n_atoms
            call compute_polymer_atoms(sys_geom, primaries(i, :), polymer_level, atom_list, n_atoms)
            primary_n_atoms(i) = n_atoms
            primary_atoms(1:n_atoms, i) = atom_list(1:n_atoms)
            deallocate (atom_list)
         end block
      end do

      ! Allocate work arrays
      allocate (clique(max_k_level))
      allocate (current_atoms(max_atoms))
      allocate (candidates(n_primaries))

      ! Start DFS from each primary
      do i = 1, n_primaries
         ! Initial clique: just primary i
         clique(1) = i
         current_atoms(1:primary_n_atoms(i)) = primary_atoms(1:primary_n_atoms(i), i)

         ! Candidates: all primaries after i (to avoid duplicates)
         n_candidates = n_primaries - i
         if (n_candidates > 0) then
            candidates(1:n_candidates) = [(i + j, j=1, n_candidates)]
         end if

         ! DFS from this primary
         call dfs_pie_accumulate(primary_atoms, primary_n_atoms, n_primaries, max_atoms, &
                                 clique, 1, current_atoms(1:primary_n_atoms(i)), primary_n_atoms(i), &
                                 candidates, n_candidates, max_k_level, &
                                 temp_atom_sets, temp_coefficients, n_pie_terms, max_terms, error)
         if (error%has_error()) return
      end do

      ! Copy to output arrays
      if (n_pie_terms > 0_int64) then
         if (n_pie_terms > int(huge(0_default_int), int64)) then
            call error%set(ERROR_VALIDATION, "n_pie_terms exceeds default integer limit")
            return
         end if
         allocate (pie_atom_sets(max_atoms, int(n_pie_terms, default_int)))
         allocate (pie_coefficients(int(n_pie_terms, default_int)))
         pie_atom_sets = temp_atom_sets(:, 1:n_pie_terms)
         pie_coefficients = temp_coefficients(1:n_pie_terms)
      end if

      call logger%info("Generated "//to_char(n_pie_terms)//" unique PIE terms")

      ! Cleanup
      deallocate (temp_atom_sets, temp_coefficients, primary_atoms, primary_n_atoms)
      deallocate (clique, current_atoms, candidates)

   end subroutine gmbe_enumerate_pie_terms

   recursive subroutine dfs_pie_accumulate(primary_atoms, primary_n_atoms, n_primaries, max_atoms, &
                                           clique, clique_size, current_atoms, n_current_atoms, &
                                           candidates, n_candidates, max_k_level, &
                                           atom_sets, coefficients, n_terms, max_terms, error)
      !! DFS helper: accumulate PIE coefficients for intersections
      integer, intent(in) :: primary_atoms(:, :)    !! Precomputed atom lists
      integer, intent(in) :: primary_n_atoms(:)     !! Atom counts
      integer, intent(in) :: n_primaries, max_atoms
      integer, intent(in) :: clique(:)              !! Current clique
      integer, intent(in) :: clique_size            !! Size of current clique
      integer, intent(in) :: current_atoms(:)       !! Atoms in current intersection
      integer, intent(in) :: n_current_atoms        !! Number of atoms in intersection
      integer, intent(in) :: candidates(:)          !! Candidate primaries
      integer, intent(in) :: n_candidates
      integer, intent(in) :: max_k_level
      integer, allocatable, intent(inout) :: atom_sets(:, :)
      integer, allocatable, intent(inout) :: coefficients(:)
      integer(int64), intent(inout) :: n_terms
      integer(int64), intent(inout) :: max_terms
      type(error_t), intent(inout) :: error         !! Error status

      integer :: sign, i, candidate_idx, candidate_pos
      integer(int64) :: term_idx
      integer, allocatable :: new_atoms(:), new_candidates(:)
      integer :: n_new_atoms, n_new_candidates
      logical :: found

      ! Skip empty intersections
      if (n_current_atoms == 0) return

      ! Compute PIE sign: (+1) for odd clique size, (-1) for even
      sign = merge(1, -1, mod(clique_size, 2) == 1)

      ! Find or create entry for this atom set
      found = .false.
      do term_idx = 1_int64, n_terms
         if (atom_sets_equal(atom_sets(:, term_idx), current_atoms, n_current_atoms)) then
            coefficients(term_idx) = coefficients(term_idx) + sign
            found = .true.
            exit
         end if
      end do

      if (.not. found) then
         ! New atom set
         if (n_terms >= max_terms) then
            call grow_pie_storage(atom_sets, coefficients, max_terms, max_atoms, error)
            if (error%has_error()) return
         end if
         n_terms = n_terms + 1_int64
         atom_sets(1:n_current_atoms, n_terms) = current_atoms(1:n_current_atoms)
         atom_sets(n_current_atoms + 1:, n_terms) = -1
         coefficients(n_terms) = sign
      end if

      ! Stop if we've reached maximum clique size
      if (clique_size >= max_k_level) return
      if (n_candidates == 0) return

      ! Try adding each candidate to the clique
      allocate (new_atoms(max_atoms))
      allocate (new_candidates(n_primaries))

      do i = 1, n_candidates
         candidate_idx = candidates(i)

         ! Compute intersection with this candidate
         call intersect_atom_lists(current_atoms, n_current_atoms, &
                                   primary_atoms(:, candidate_idx), primary_n_atoms(candidate_idx), &
                                   new_atoms, n_new_atoms)

         ! Skip if no intersection
         if (n_new_atoms == 0) cycle

         ! New candidates: must come after this one and overlap with new_atoms
         n_new_candidates = 0
         do candidate_pos = i + 1, n_candidates
            block
               integer :: test_candidate, test_n_intersect
               integer, allocatable :: test_intersect(:)
               test_candidate = candidates(candidate_pos)

               allocate (test_intersect(max_atoms))
               call intersect_atom_lists(new_atoms, n_new_atoms, &
                                         primary_atoms(:, test_candidate), primary_n_atoms(test_candidate), &
                                         test_intersect, test_n_intersect)

               if (test_n_intersect > 0) then
                  n_new_candidates = n_new_candidates + 1
                  new_candidates(n_new_candidates) = test_candidate
               end if
               deallocate (test_intersect)
            end block
         end do

         ! Recurse
         block
            integer :: new_clique(clique_size + 1)
            new_clique(1:clique_size) = clique(1:clique_size)
            new_clique(clique_size + 1) = candidate_idx

            call dfs_pie_accumulate(primary_atoms, primary_n_atoms, n_primaries, max_atoms, &
                                    new_clique, clique_size + 1, new_atoms, n_new_atoms, &
                                    new_candidates, n_new_candidates, max_k_level, &
                                    atom_sets, coefficients, n_terms, max_terms, error)
            if (error%has_error()) then
               deallocate (new_atoms, new_candidates)
               return
            end if
         end block
      end do

      deallocate (new_atoms, new_candidates)

   end subroutine dfs_pie_accumulate

   subroutine grow_pie_storage(atom_sets, coefficients, max_terms, max_atoms, error)
      !! Grow PIE term storage arrays when capacity is exceeded.
      integer, allocatable, intent(inout) :: atom_sets(:, :)
      integer, allocatable, intent(inout) :: coefficients(:)
      integer(int64), intent(inout) :: max_terms
      integer, intent(in) :: max_atoms
      type(error_t), intent(inout) :: error

      integer(int64) :: new_max_terms
      integer(default_int) :: old_terms_i, new_terms_i
      integer, allocatable :: new_atom_sets(:, :)
      integer, allocatable :: new_coefficients(:)
      integer(int64) :: max_default_int

      max_default_int = int(huge(0_default_int), int64)
      new_max_terms = max_terms*2_int64
      if (new_max_terms <= max_terms) then
         call error%set(ERROR_VALIDATION, "PIE term capacity overflow")
         return
      end if

      if (new_max_terms > max_default_int) then
         new_max_terms = max_default_int
      end if

      if (new_max_terms == max_terms) then
         call error%set(ERROR_VALIDATION, "Exceeded maximum PIE terms ("//to_char(max_terms)//")")
         return
      end if

      old_terms_i = int(max_terms, default_int)
      new_terms_i = int(new_max_terms, default_int)

      allocate (new_atom_sets(max_atoms, new_terms_i))
      new_atom_sets = -1
      new_atom_sets(:, 1:old_terms_i) = atom_sets(:, 1:old_terms_i)

      allocate (new_coefficients(new_terms_i))
      new_coefficients = 0
      new_coefficients(1:old_terms_i) = coefficients(1:old_terms_i)

      call move_alloc(new_atom_sets, atom_sets)
      call move_alloc(new_coefficients, coefficients)
      max_terms = new_max_terms
   end subroutine grow_pie_storage

   pure function atom_sets_equal(set1, set2, n_atoms) result(equal)
      !! Check if two atom sets are equal (assuming sorted)
      integer, intent(in) :: set1(:), set2(:)
      integer, intent(in) :: n_atoms
      logical :: equal
      integer :: i

      equal = .true.
      do i = 1, n_atoms
         if (set1(i) /= set2(i)) then
            equal = .false.
            return
         end if
      end do
   end function atom_sets_equal

   pure subroutine intersect_atom_lists(atoms1, n1, atoms2, n2, intersection, n_intersect)
      !! Compute intersection of two atom lists
      integer, intent(in) :: atoms1(:), n1, atoms2(:), n2
      integer, intent(out) :: intersection(:)
      integer, intent(out) :: n_intersect
      integer :: i, j

      n_intersect = 0
      do i = 1, n1
         if (atoms1(i) < 0) cycle
         do j = 1, n2
            if (atoms2(j) < 0) cycle
            if (atoms1(i) == atoms2(j)) then
               n_intersect = n_intersect + 1
               intersection(n_intersect) = atoms1(i)
               exit
            end if
         end do
      end do
   end subroutine intersect_atom_lists

   !---------------------------------------------------------------------------
   ! GMBE Output/Printing subroutines
   !---------------------------------------------------------------------------

   subroutine print_gmbe_energy_breakdown(monomer_energy, n_monomers, level_energies, level_counts, &
                                          max_level, total_energy, has_intersections)
      !! Print GMBE energy breakdown using inclusion-exclusion principle
      real(dp), intent(in) :: monomer_energy
      integer, intent(in) :: n_monomers
      real(dp), intent(in), optional :: level_energies(:)
      integer, intent(in), optional :: level_counts(:)
      integer, intent(in) :: max_level
      real(dp), intent(in) :: total_energy
      logical, intent(in) :: has_intersections

      integer :: k
      real(dp) :: sign_factor
      character(len=256) :: line

      if (has_intersections) then
         call logger%info("GMBE Energy breakdown (Inclusion-Exclusion Principle):")
         write (line, '(a,i0,a,f20.10)') "  Monomers (", n_monomers, "):  ", monomer_energy
         call logger%info(trim(line))

         do k = 2, max_level
            if (level_counts(k) > 0) then
               sign_factor = real((-1)**(k + 1), dp)
               if (sign_factor > 0.0_dp) then
                  write (line, '(a,i0,a,i0,a,f20.10)') "  ", k, "-way ∩ (", level_counts(k), "):  +", level_energies(k)
               else
                  write (line, '(a,i0,a,i0,a,f20.10)') "  ", k, "-way ∩ (", level_counts(k), "):  ", level_energies(k)
               end if
               call logger%info(trim(line))
            end if
         end do

         write (line, '(a,f20.10)') "  Total GMBE:      ", total_energy
         call logger%info(trim(line))
      else
         call logger%info("GMBE Energy breakdown:")
         write (line, '(a,i0,a,f20.10)') "  Monomers (", n_monomers, "): ", monomer_energy
         call logger%info(trim(line))
         write (line, '(a,f20.10)') "  Total GMBE:  ", total_energy
         call logger%info(trim(line))
      end if
   end subroutine print_gmbe_energy_breakdown

   subroutine print_gmbe_gradient_info(total_gradient, sys_geom, current_log_level)
      !! Print GMBE gradient information
      real(dp), intent(in) :: total_gradient(:, :)
      type(system_geometry_t), intent(in) :: sys_geom
      integer, intent(in) :: current_log_level

      integer :: iatom
      character(len=256) :: grad_line

      call logger%info("GMBE gradient computation completed")
      call logger%info("  Total gradient norm: "//to_char(sqrt(sum(total_gradient**2))))

      if (current_log_level >= info_level .and. sys_geom%total_atoms < 100) then
         call logger%info(" ")
         call logger%info("Total GMBE Gradient (Hartree/Bohr):")
         do iatom = 1, sys_geom%total_atoms
            write (grad_line, '(a,i5,a,3f20.12)') "  Atom ", iatom, ": ", &
               total_gradient(1, iatom), total_gradient(2, iatom), total_gradient(3, iatom)
            call logger%info(trim(grad_line))
         end do
         call logger%info(" ")
      end if
   end subroutine print_gmbe_gradient_info

   subroutine print_gmbe_intersection_debug(n_intersections, n_monomers, intersection_sets, &
                                            intersection_levels, intersection_results)
      !! Print debug information about GMBE intersections
      integer, intent(in) :: n_intersections, n_monomers
      integer, intent(in) :: intersection_sets(:, :)
      integer, intent(in) :: intersection_levels(:)
      type(calculation_result_t), intent(in) :: intersection_results(:)

      integer :: i, j, set_size
      real(dp) :: sign_factor
      character(len=512) :: detail_line
      character(len=256) :: set_str

      if (n_intersections > 0) then
         call logger%debug("GMBE intersection details:")
         do i = 1, n_intersections
            set_str = "("
            set_size = 0
            do j = 1, n_monomers
               if (intersection_sets(j, i) > 0) then
                  if (set_size > 0) set_str = trim(set_str)//","
                  write (set_str, '(a,i0)') trim(set_str), intersection_sets(j, i)
                  set_size = set_size + 1
               end if
            end do
            set_str = trim(set_str)//")"

            sign_factor = real((-1)**(intersection_levels(i) + 1), dp)
            write (detail_line, '(a,i0,a,i0,a,a,a,f16.8)') &
               "  Intersection ", i, ": level=", intersection_levels(i), &
               " fragments=", trim(set_str), " energy=", intersection_results(i)%energy%total()
            call logger%debug(trim(detail_line))
         end do
      end if
   end subroutine print_gmbe_intersection_debug

end module mqc_gmbe_utils