mqc_json_writer.f90 Source File

Centralized JSON output writer Single entry point for all JSON output in metalquicha


This file depends on

sourcefile~~mqc_json_writer.f90~~EfferentGraph sourcefile~mqc_json_writer.f90 mqc_json_writer.f90 sourcefile~mqc_io_helpers.f90 mqc_io_helpers.f90 sourcefile~mqc_json_writer.f90->sourcefile~mqc_io_helpers.f90 sourcefile~mqc_json_output_types.f90 mqc_json_output_types.f90 sourcefile~mqc_json_writer.f90->sourcefile~mqc_json_output_types.f90 sourcefile~mqc_mbe_io.f90 mqc_mbe_io.f90 sourcefile~mqc_json_writer.f90->sourcefile~mqc_mbe_io.f90 sourcefile~mqc_physical_constants.f90 mqc_physical_constants.f90 sourcefile~mqc_json_writer.f90->sourcefile~mqc_physical_constants.f90 sourcefile~mqc_program_limits.f90 mqc_program_limits.f90 sourcefile~mqc_json_writer.f90->sourcefile~mqc_program_limits.f90 sourcefile~mqc_thermochemistry.f90 mqc_thermochemistry.f90 sourcefile~mqc_json_output_types.f90->sourcefile~mqc_thermochemistry.f90 sourcefile~mqc_elements.f90 mqc_elements.f90 sourcefile~mqc_mbe_io.f90->sourcefile~mqc_elements.f90 sourcefile~mqc_physical_fragment.f90 mqc_physical_fragment.f90 sourcefile~mqc_mbe_io.f90->sourcefile~mqc_physical_fragment.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_physical_constants.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_elements.f90 sourcefile~mqc_cgto.f90 mqc_cgto.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_cgto.f90 sourcefile~mqc_error.f90 mqc_error.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_error.f90 sourcefile~mqc_geometry.f90 mqc_geometry.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_geometry.f90 sourcefile~mqc_xyz_reader.f90 mqc_xyz_reader.f90 sourcefile~mqc_physical_fragment.f90->sourcefile~mqc_xyz_reader.f90 sourcefile~mqc_thermochemistry.f90->sourcefile~mqc_physical_constants.f90 sourcefile~mqc_thermochemistry.f90->sourcefile~mqc_elements.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_json_writer.f90~~AfferentGraph sourcefile~mqc_json_writer.f90 mqc_json_writer.f90 sourcefile~mqc_driver.f90 mqc_driver.f90 sourcefile~mqc_driver.f90->sourcefile~mqc_json_writer.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

Source Code

!! Centralized JSON output writer
!! Single entry point for all JSON output in metalquicha
module mqc_json_writer
   use pic_types, only: int64, dp
   use pic_logger, only: logger => global_logger
   use mqc_json_output_types, only: json_output_data_t, &
                                    OUTPUT_MODE_UNFRAGMENTED, OUTPUT_MODE_MBE, OUTPUT_MODE_GMBE_PIE
   use mqc_io_helpers, only: get_output_json_filename, get_basename
   use mqc_physical_constants, only: HARTREE_TO_CALMOL, R_CALMOLK, AU_TO_DEBYE, CAL_TO_J
   use mqc_program_limits, only: JSON_REAL_FORMAT
   use mqc_mbe_io, only: get_frag_level_name
   use json_module, only: json_core, json_value
   implicit none
   private

   public :: write_json_output  !! Single entry point for all JSON output

contains

   subroutine write_json_output(output_data)
      !! THE single entry point for all JSON output
      !!
      !! Dispatches to the appropriate JSON writer based on output_mode.
      !! This is the ONLY place in the codebase where JSON files are written.
      type(json_output_data_t), intent(in) :: output_data

      select case (output_data%output_mode)
      case (OUTPUT_MODE_UNFRAGMENTED)
         if (output_data%has_vibrational) then
            call write_vibrational_json_impl(output_data)
         else
            call write_unfragmented_json_impl(output_data)
         end if

      case (OUTPUT_MODE_MBE)
         if (output_data%has_vibrational) then
            call write_vibrational_json_impl(output_data)
         else
            call write_mbe_breakdown_json_impl(output_data)
         end if

      case (OUTPUT_MODE_GMBE_PIE)
         if (output_data%has_vibrational) then
            call write_vibrational_json_impl(output_data)
         else
            call write_gmbe_pie_json_impl(output_data)
         end if

      case default
         call logger%error("Unknown output mode in write_json_output")
      end select

   end subroutine write_json_output

   subroutine write_unfragmented_json_impl(data)
      !! Write unfragmented calculation results to output JSON file
      type(json_output_data_t), intent(in) :: data

      type(json_core) :: json
      type(json_value), pointer :: root, main_obj, dipole_obj
      integer :: iunit, io_stat
      character(len=256) :: output_file, basename

      output_file = get_output_json_filename()
      basename = get_basename()

      call json%initialize(real_format=JSON_REAL_FORMAT)
      call json%create_object(root, '')
      call json%create_object(main_obj, trim(basename))
      call json%add(root, main_obj)

      if (data%has_energy) call json%add(main_obj, 'total_energy', data%total_energy)

      if (data%has_dipole .and. allocated(data%dipole)) then
         call json%create_object(dipole_obj, 'dipole')
         call json%add(main_obj, dipole_obj)
         call json%add(dipole_obj, 'x', data%dipole(1))
         call json%add(dipole_obj, 'y', data%dipole(2))
         call json%add(dipole_obj, 'z', data%dipole(3))
         call json%add(dipole_obj, 'magnitude_debye', norm2(data%dipole)*AU_TO_DEBYE)
      end if

      if (data%has_gradient .and. allocated(data%gradient)) then
         call json%add(main_obj, 'gradient_norm', sqrt(sum(data%gradient**2)))
      end if
      if (data%has_hessian .and. allocated(data%hessian)) then
         call json%add(main_obj, 'hessian_frobenius_norm', sqrt(sum(data%hessian**2)))
      end if

      call logger%info("Writing JSON output to "//trim(output_file))
      open (newunit=iunit, file=trim(output_file), status='replace', action='write', iostat=io_stat)
      if (io_stat /= 0) then
         call logger%error("Failed to open "//trim(output_file)//" for writing")
         call json%destroy(root)
         return
      end if

      call json%print(root, iunit)
      close (iunit)
      call json%destroy(root)
      call logger%info("JSON output written successfully to "//trim(output_file))

   end subroutine write_unfragmented_json_impl

   subroutine write_mbe_breakdown_json_impl(data)
      !! Write detailed MBE energy breakdown to JSON file
      type(json_output_data_t), intent(in) :: data

      type(json_core) :: json
      type(json_value), pointer :: root, main_obj, levels_arr, level_obj, frags_arr, frag_obj
      type(json_value), pointer :: dipole_obj
      integer(int64) :: i, count_by_level
      integer :: fragment_size, j, frag_level, iunit, io_stat
      integer, allocatable :: indices(:)
      character(len=32) :: level_name
      character(len=256) :: output_file, basename

      output_file = get_output_json_filename()
      basename = get_basename()

      if (data%max_level > 10) then
         call logger%warning("Fragment levels exceed decamers (10-mers). JSON will use generic N-mers notation.")
      end if

      call json%initialize(real_format=JSON_REAL_FORMAT)
      call json%create_object(root, '')
      call json%create_object(main_obj, trim(basename))
      call json%add(root, main_obj)

      call json%add(main_obj, 'total_energy', data%total_energy)

      ! Build levels array
      call json%create_array(levels_arr, 'levels')
      call json%add(main_obj, levels_arr)

      do frag_level = 1, data%max_level
         count_by_level = 0_int64
         do i = 1_int64, data%fragment_count
            fragment_size = count(data%polymers(i, :) > 0)
            if (fragment_size == frag_level) count_by_level = count_by_level + 1_int64
         end do

         if (count_by_level > 0_int64) then
            call json%create_object(level_obj, '')
            call json%add(levels_arr, level_obj)

            level_name = get_frag_level_name(frag_level)
            call json%add(level_obj, 'frag_level', frag_level)
            call json%add(level_obj, 'name', trim(level_name))
            call json%add(level_obj, 'count', int(count_by_level))
            if (allocated(data%sum_by_level)) then
               call json%add(level_obj, 'total_energy', data%sum_by_level(frag_level))
            end if

            call json%create_array(frags_arr, 'fragments')
            call json%add(level_obj, frags_arr)

            do i = 1_int64, data%fragment_count
               fragment_size = count(data%polymers(i, :) > 0)
               if (fragment_size == frag_level) then
                  call json%create_object(frag_obj, '')
                  call json%add(frags_arr, frag_obj)

                  allocate (indices(fragment_size))
                  indices = data%polymers(i, 1:fragment_size)
                  call json%add(frag_obj, 'indices', indices)
                  deallocate (indices)

                  if (allocated(data%fragment_energies)) then
                     call json%add(frag_obj, 'energy', data%fragment_energies(i))
                  end if

                  if (allocated(data%fragment_distances)) then
                     call json%add(frag_obj, 'distance', data%fragment_distances(i))
                  end if

                  if (frag_level > 1 .and. allocated(data%delta_energies)) then
                     call json%add(frag_obj, 'delta_energy', data%delta_energies(i))
                  end if
               end if
            end do
         end if
      end do

      ! Add dipole if computed
      if (data%has_dipole .and. allocated(data%dipole)) then
         call json%create_object(dipole_obj, 'dipole')
         call json%add(main_obj, dipole_obj)
         call json%add(dipole_obj, 'x', data%dipole(1))
         call json%add(dipole_obj, 'y', data%dipole(2))
         call json%add(dipole_obj, 'z', data%dipole(3))
         call json%add(dipole_obj, 'magnitude_debye', norm2(data%dipole)*AU_TO_DEBYE)
      end if

      if (data%has_gradient .and. allocated(data%gradient)) then
         call json%add(main_obj, 'gradient_norm', sqrt(sum(data%gradient**2)))
      end if

      if (data%has_hessian .and. allocated(data%hessian)) then
         call json%add(main_obj, 'hessian_frobenius_norm', sqrt(sum(data%hessian**2)))
      end if

      ! Write to file
      call logger%info("Writing JSON output to "//trim(output_file))
      open (newunit=iunit, file=trim(output_file), status='replace', action='write', iostat=io_stat)
      if (io_stat /= 0) then
         call logger%error("Failed to open "//trim(output_file)//" for writing")
         call json%destroy(root)
         return
      end if

      call json%print(root, iunit)
      close (iunit)
      call json%destroy(root)
      call logger%info("JSON output written successfully to "//trim(output_file))

   end subroutine write_mbe_breakdown_json_impl

   subroutine write_gmbe_pie_json_impl(data)
      !! Write GMBE PIE calculation results to output JSON file
      type(json_output_data_t), intent(in) :: data

      type(json_core) :: json
      type(json_value), pointer :: root, main_obj, pie_obj, terms_arr, term_obj
      integer :: j, max_atoms, n_atoms, iunit, io_stat
      integer(int64) :: i, n_nonzero_terms
      integer, allocatable :: atom_indices(:)
      character(len=256) :: output_file, basename

      output_file = get_output_json_filename()
      basename = get_basename()

      call json%initialize(real_format=JSON_REAL_FORMAT)
      call json%create_object(root, '')
      call json%create_object(main_obj, trim(basename))
      call json%add(root, main_obj)

      call json%add(main_obj, 'total_energy', data%total_energy)

      if (data%has_gradient .and. allocated(data%gradient)) then
         call json%add(main_obj, 'gradient_norm', sqrt(sum(data%gradient**2)))
      end if
      if (data%has_hessian .and. allocated(data%hessian)) then
         call json%add(main_obj, 'hessian_frobenius_norm', sqrt(sum(data%hessian**2)))
      end if

      ! Count non-zero coefficient terms
      if (allocated(data%pie_coefficients)) then
         n_nonzero_terms = count(data%pie_coefficients(1:data%n_pie_terms) /= 0)
      else
         n_nonzero_terms = 0
      end if

      ! PIE terms section
      call json%create_object(pie_obj, 'pie_terms')
      call json%add(main_obj, pie_obj)
      call json%add(pie_obj, 'count', int(n_nonzero_terms))

      call json%create_array(terms_arr, 'terms')
      call json%add(pie_obj, terms_arr)

      if (allocated(data%pie_atom_sets) .and. allocated(data%pie_coefficients) .and. &
          allocated(data%pie_energies)) then
         max_atoms = size(data%pie_atom_sets, 1)

         do i = 1_int64, data%n_pie_terms
            if (data%pie_coefficients(i) == 0) cycle

            call json%create_object(term_obj, '')
            call json%add(terms_arr, term_obj)

            ! Extract atom list size (atoms until negative sentinel)
            n_atoms = 0
            do while (n_atoms < max_atoms .and. data%pie_atom_sets(n_atoms + 1, i) >= 0)
               n_atoms = n_atoms + 1
            end do

            allocate (atom_indices(n_atoms))
            atom_indices = data%pie_atom_sets(1:n_atoms, i)
            call json%add(term_obj, 'atom_indices', atom_indices)
            deallocate (atom_indices)

            call json%add(term_obj, 'coefficient', data%pie_coefficients(i))
            call json%add(term_obj, 'energy', data%pie_energies(i))
            call json%add(term_obj, 'weighted_energy', real(data%pie_coefficients(i), dp)*data%pie_energies(i))
         end do
      end if

      ! Write to file
      call logger%info("Writing GMBE PIE JSON output to "//trim(output_file))
      open (newunit=iunit, file=trim(output_file), status='replace', action='write', iostat=io_stat)
      if (io_stat /= 0) then
         call logger%error("Failed to open "//trim(output_file)//" for writing")
         call json%destroy(root)
         return
      end if

      call json%print(root, iunit)
      close (iunit)
      call json%destroy(root)
      call logger%info("GMBE PIE JSON output written successfully to "//trim(output_file))

   end subroutine write_gmbe_pie_json_impl

   subroutine write_vibrational_json_impl(data)
      !! Write vibrational analysis and thermochemistry results to JSON file
      type(json_output_data_t), intent(in) :: data

      type(json_core) :: json
      type(json_value), pointer :: root, main_obj, dipole_obj, vib_obj, thermo_obj
      type(json_value), pointer :: moi_obj, rot_obj, pf_obj, contrib_obj, table_obj
      type(json_value), pointer :: trans_obj, rot_contrib, vib_contrib, elec_obj
      type(json_value), pointer :: vib_row, rot_row, int_row, tr_row, tot_row
      type(json_value), pointer :: thermal_obj, total_e_obj
      integer :: io_stat, iunit
      character(len=256) :: output_file, basename
      real(dp) :: pV_cal, H_vib_cal, H_rot_cal, H_trans_cal, H_total_cal
      real(dp) :: Cv_total, S_total, S_total_J, H_int_cal, Cv_int, S_int, S_int_J, Cp_trans
      real(dp) :: grad_norm, hess_norm

      output_file = get_output_json_filename()
      basename = get_basename()

      call json%initialize(real_format=JSON_REAL_FORMAT)

      ! Create root object
      call json%create_object(root, '')

      ! Create main object with basename as key
      call json%create_object(main_obj, trim(basename))
      call json%add(root, main_obj)

      ! Total energy
      if (data%has_energy) call json%add(main_obj, 'total_energy', data%total_energy)

      ! Dipole
      if (data%has_dipole .and. allocated(data%dipole)) then
         call json%create_object(dipole_obj, 'dipole')
         call json%add(main_obj, dipole_obj)
         call json%add(dipole_obj, 'x', data%dipole(1))
         call json%add(dipole_obj, 'y', data%dipole(2))
         call json%add(dipole_obj, 'z', data%dipole(3))
         call json%add(dipole_obj, 'magnitude_debye', norm2(data%dipole)*AU_TO_DEBYE)
      end if

      ! Gradient and Hessian norms
      if (data%has_gradient .and. allocated(data%gradient)) then
         grad_norm = sqrt(sum(data%gradient**2))
         call json%add(main_obj, 'gradient_norm', grad_norm)
      end if
      if (data%has_hessian .and. allocated(data%hessian)) then
         hess_norm = sqrt(sum(data%hessian**2))
         call json%add(main_obj, 'hessian_frobenius_norm', hess_norm)
      end if

      ! Vibrational analysis section
      call json%create_object(vib_obj, 'vibrational_analysis')
      call json%add(main_obj, vib_obj)
      if (allocated(data%frequencies)) then
         call json%add(vib_obj, 'n_modes', size(data%frequencies))
         call json%add(vib_obj, 'frequencies_cm1', data%frequencies)
      end if
      if (allocated(data%reduced_masses)) then
         call json%add(vib_obj, 'reduced_masses_amu', data%reduced_masses)
      end if
      if (allocated(data%force_constants)) then
         call json%add(vib_obj, 'force_constants_mdyne_ang', data%force_constants)
      end if
      if (data%has_ir_intensities .and. allocated(data%ir_intensities)) then
         call json%add(vib_obj, 'ir_intensities_km_mol', data%ir_intensities)
      end if

      ! Thermochemistry section
      call json%create_object(thermo_obj, 'thermochemistry')
      call json%add(main_obj, thermo_obj)

      ! Conditions
      call json%add(thermo_obj, 'temperature_K', data%thermo%temperature)
      call json%add(thermo_obj, 'pressure_atm', data%thermo%pressure)
      call json%add(thermo_obj, 'molecular_mass_amu', data%thermo%total_mass)
      call json%add(thermo_obj, 'symmetry_number', data%thermo%symmetry_number)
      call json%add(thermo_obj, 'spin_multiplicity', data%thermo%spin_multiplicity)
      call json%add(thermo_obj, 'is_linear', data%thermo%is_linear)
      call json%add(thermo_obj, 'n_real_frequencies', data%thermo%n_real_freqs)
      call json%add(thermo_obj, 'n_imaginary_frequencies', data%thermo%n_imag_freqs)

      ! Moments of inertia
      call json%create_object(moi_obj, 'moments_of_inertia_amu_ang2')
      call json%add(thermo_obj, moi_obj)
      call json%add(moi_obj, 'Ia', data%thermo%moments(1))
      call json%add(moi_obj, 'Ib', data%thermo%moments(2))
      call json%add(moi_obj, 'Ic', data%thermo%moments(3))

      ! Rotational constants
      call json%create_object(rot_obj, 'rotational_constants_GHz')
      call json%add(thermo_obj, rot_obj)
      call json%add(rot_obj, 'A', data%thermo%rot_const(1))
      call json%add(rot_obj, 'B', data%thermo%rot_const(2))
      call json%add(rot_obj, 'C', data%thermo%rot_const(3))

      ! Partition functions
      call json%create_object(pf_obj, 'partition_functions')
      call json%add(thermo_obj, pf_obj)
      call json%add(pf_obj, 'translational', data%thermo%q_trans)
      call json%add(pf_obj, 'rotational', data%thermo%q_rot)
      call json%add(pf_obj, 'vibrational', data%thermo%q_vib)

      ! Thermodynamic contributions
      call json%create_object(contrib_obj, 'contributions')
      call json%add(thermo_obj, contrib_obj)

      call json%create_object(trans_obj, 'translational')
      call json%add(contrib_obj, trans_obj)
      call json%add(trans_obj, 'energy_hartree', data%thermo%E_trans)
      call json%add(trans_obj, 'entropy_cal_mol_K', data%thermo%S_trans)
      call json%add(trans_obj, 'Cv_cal_mol_K', data%thermo%Cv_trans)

      call json%create_object(rot_contrib, 'rotational')
      call json%add(contrib_obj, rot_contrib)
      call json%add(rot_contrib, 'energy_hartree', data%thermo%E_rot)
      call json%add(rot_contrib, 'entropy_cal_mol_K', data%thermo%S_rot)
      call json%add(rot_contrib, 'Cv_cal_mol_K', data%thermo%Cv_rot)

      call json%create_object(vib_contrib, 'vibrational')
      call json%add(contrib_obj, vib_contrib)
      call json%add(vib_contrib, 'energy_hartree', data%thermo%E_vib)
      call json%add(vib_contrib, 'entropy_cal_mol_K', data%thermo%S_vib)
      call json%add(vib_contrib, 'Cv_cal_mol_K', data%thermo%Cv_vib)

      call json%create_object(elec_obj, 'electronic')
      call json%add(contrib_obj, elec_obj)
      call json%add(elec_obj, 'energy_hartree', data%thermo%E_elec)
      call json%add(elec_obj, 'entropy_cal_mol_K', data%thermo%S_elec)

      ! Contribution table
      pV_cal = R_CALMOLK*data%thermo%temperature
      H_vib_cal = data%thermo%E_vib*HARTREE_TO_CALMOL
      H_rot_cal = data%thermo%E_rot*HARTREE_TO_CALMOL
      H_trans_cal = data%thermo%E_trans*HARTREE_TO_CALMOL + pV_cal
      H_total_cal = H_vib_cal + H_rot_cal + H_trans_cal
      H_int_cal = H_vib_cal + H_rot_cal
      Cp_trans = data%thermo%Cv_trans + R_CALMOLK
      Cv_int = data%thermo%Cv_vib + data%thermo%Cv_rot
      Cv_total = Cp_trans + data%thermo%Cv_rot + data%thermo%Cv_vib
      S_int = data%thermo%S_vib + data%thermo%S_rot
      S_int_J = S_int*CAL_TO_J
      S_total = data%thermo%S_trans + data%thermo%S_rot + data%thermo%S_vib + data%thermo%S_elec
      S_total_J = S_total*CAL_TO_J

      call json%create_object(table_obj, 'contribution_table')
      call json%add(thermo_obj, table_obj)

      call json%create_object(vib_row, 'VIB')
      call json%add(table_obj, vib_row)
      call json%add(vib_row, 'H_cal_mol', H_vib_cal)
      call json%add(vib_row, 'Cp_cal_mol_K', data%thermo%Cv_vib)
      call json%add(vib_row, 'S_cal_mol_K', data%thermo%S_vib)
      call json%add(vib_row, 'S_J_mol_K', data%thermo%S_vib*CAL_TO_J)

      call json%create_object(rot_row, 'ROT')
      call json%add(table_obj, rot_row)
      call json%add(rot_row, 'H_cal_mol', H_rot_cal)
      call json%add(rot_row, 'Cp_cal_mol_K', data%thermo%Cv_rot)
      call json%add(rot_row, 'S_cal_mol_K', data%thermo%S_rot)
      call json%add(rot_row, 'S_J_mol_K', data%thermo%S_rot*CAL_TO_J)

      call json%create_object(int_row, 'INT')
      call json%add(table_obj, int_row)
      call json%add(int_row, 'H_cal_mol', H_int_cal)
      call json%add(int_row, 'Cp_cal_mol_K', Cv_int)
      call json%add(int_row, 'S_cal_mol_K', S_int)
      call json%add(int_row, 'S_J_mol_K', S_int_J)

      call json%create_object(tr_row, 'TR')
      call json%add(table_obj, tr_row)
      call json%add(tr_row, 'H_cal_mol', H_trans_cal)
      call json%add(tr_row, 'Cp_cal_mol_K', Cp_trans)
      call json%add(tr_row, 'S_cal_mol_K', data%thermo%S_trans)
      call json%add(tr_row, 'S_J_mol_K', data%thermo%S_trans*CAL_TO_J)

      call json%create_object(tot_row, 'TOT')
      call json%add(table_obj, tot_row)
      call json%add(tot_row, 'H_cal_mol', H_total_cal)
      call json%add(tot_row, 'Cp_cal_mol_K', Cv_total)
      call json%add(tot_row, 'S_cal_mol_K', S_total)
      call json%add(tot_row, 'S_J_mol_K', S_total_J)

      ! Zero-point energy
      call json%add(thermo_obj, 'zero_point_energy_hartree', data%thermo%zpe_hartree)
      call json%add(thermo_obj, 'zero_point_energy_kcal_mol', data%thermo%zpe_kcalmol)

      ! Thermal corrections
      call json%create_object(thermal_obj, 'thermal_corrections_hartree')
      call json%add(thermo_obj, thermal_obj)
      call json%add(thermal_obj, 'to_energy', data%thermo%thermal_correction_energy)
      call json%add(thermal_obj, 'to_enthalpy', data%thermo%thermal_correction_enthalpy)
      call json%add(thermal_obj, 'to_gibbs', data%thermo%thermal_correction_gibbs)

      ! Total energies
      call json%create_object(total_e_obj, 'total_energies_hartree')
      call json%add(thermo_obj, total_e_obj)
      call json%add(total_e_obj, 'electronic', data%total_energy)
      call json%add(total_e_obj, 'electronic_plus_zpe', data%total_energy + data%thermo%zpe_hartree)
      call json%add(total_e_obj, 'electronic_plus_thermal_E', data%total_energy + data%thermo%thermal_correction_energy)
      call json%add(total_e_obj, 'electronic_plus_thermal_H', data%total_energy + data%thermo%thermal_correction_enthalpy)
      call json%add(total_e_obj, 'electronic_plus_thermal_G', data%total_energy + data%thermo%thermal_correction_gibbs)

      ! Write to file
      call logger%info("Writing vibrational/thermochemistry JSON to "//trim(output_file))
      open (newunit=iunit, file=trim(output_file), status='replace', action='write', iostat=io_stat)
      if (io_stat /= 0) then
         call logger%error("Failed to open "//trim(output_file)//" for writing")
         call json%destroy(root)
         return
      end if

      call json%print(root, iunit)
      close (iunit)

      call json%destroy(root)
      call logger%info("Vibrational/thermochemistry JSON written successfully to "//trim(output_file))

   end subroutine write_vibrational_json_impl

end module mqc_json_writer