Example #1
0
 def setUp(self):
     pymagen_sodium = Molecule(species=['Na'],
                               coords=[[999.0, 999.0, 999.0]],
                               charge=1)
     # noinspection PyProtectedMember
     sodium_obmol = BabelMolAdaptor(pymagen_sodium)._obmol
     acetoxyq_anion_qcout = QcOutput(
         os.path.join(test_dir, "acetoxyq_anion.out"))
     pymatgen_acetoxyq = acetoxyq_anion_qcout.data[0]["molecules"][-1]
     acetoxyq_obmol = BabelMolAdaptor(pymatgen_acetoxyq)._obmol
     tfsi_qcout = QcOutput(os.path.join(test_dir, "tfsi.qcout"))
     pymatgen_tfsi = tfsi_qcout.data[0]["molecules"][-1]
     # noinspection PyProtectedMember
     tfsi_obmol = BabelMolAdaptor(pymatgen_tfsi)._obmol
     fragments = [sodium_obmol, tfsi_obmol]
     nums_fragments = [1, 1]
     self.acetoxyq_natfsi_placer = IonPlacer(acetoxyq_obmol, fragments,
                                             nums_fragments, None)
     rad_util = AtomicRadiusUtils(covalent_radius_scale=3.0,
                                  metal_radius_scale=1.5)
     mol_radius = rad_util.get_radius(acetoxyq_obmol)
     cation_radius = rad_util.get_radius(sodium_obmol)
     anion_radius = rad_util.get_radius(tfsi_obmol)
     mol_coords = IonPlacer.normalize_molecule(acetoxyq_obmol)
     fragments_atom_radius = [cation_radius, anion_radius]
     nums_fragments = [1, 1]
     self.detector = ContactDetector(mol_coords, mol_radius,
                                     fragments_atom_radius, nums_fragments)
Example #2
0
 def test_structural_change_in_geom_opt(self):
     qcout_path = os.path.join(test_dir, "mol_1_3_bond.qcout")
     qcout = QcOutput(qcout_path)
     mol1 = qcout.data[0]["molecules"][0]
     mol2 = qcout.data[0]["molecules"][-1]
     priority_bonds = [[0, 1], [0, 2], [1, 3], [1, 4], [1, 7], [2, 5], [2, 6], [2, 8], [4, 6], [4, 10], [6, 9]]
     msc = MoleculeStructureComparator(priority_bonds=priority_bonds)
     self.assertTrue(msc.are_equal(mol1, mol2))
Example #3
0
 def check(self):
     # Checks output file for errors.
     self.outdata = QcOutput(self.output_file).data
     self.qcinp = QcInput.from_file(self.input_file)
     self.error_step_id = None
     self.errors = None
     self.fix_step = None
     for i, od in enumerate(self.outdata):
         if od["has_error"]:
             self.error_step_id = i
             self.fix_step = self.qcinp.jobs[i]
             self.errors = sorted(list(set(od["errors"])))
             return True
     return False
Example #4
0
 def test_get_basis(self):
     filename = os.path.join(test_dir, "quinoxaline_anion.qcout")
     qcout = QcOutput(filename)
     charges = qcout.data[0]["charges"]["nbo"]
     mol = qcout.data[0]["molecules"][-1]
     basis = self.generator.get_basis(mol, charges)
     ans = [('C', '6-31G*'), ('C', '6-31G*'), ('C', '6-31G*'),
            ('C', '6-31G*'), ('C', '6-31G*'), ('C', '6-31G*'),
            ('H', '6-31G*'), ('H', '6-31G*'), ('H', '6-31G*'),
            ('H', '6-31G*'), ('C', '6-31G*'), ('C', '6-31G*'),
            ('H', '6-31G*'), ('N', '6-31+G*'), ('N', '6-31+G*'),
            ('H', '6-31G*')]
     self.maxDiff = None
     self.assertEqual(basis, [(b[0], b[1].lower()) for b in ans])
Example #5
0
def main():
    def gcd(a, b):
        if b == 0:
            return a
        else:
            return gcd(b, a % b)

    def lcm(a, b):
        return a * b / gcd(a, b)

    import argparse
    parser = argparse.ArgumentParser(
        description="Place salt around a molecule")
    parser.add_argument("-m", "--molecule", dest="molecule", type=str,
                        required=True,
                        help="the file name of molecule")
    parser.add_argument("-l", "--ligand", dest="fragments", type=str,
                        nargs='+',
                        required=True,
                        help="the list of fragment file names to to be placed around the molecule")
    parser.add_argument("-n", "--nums_fragments", dest="nums_fragments",
                        type=int, nargs='+',
                        required=True,
                        help="the number of each fragment, the order must be the same with FRAGMENTS")
    parser.add_argument("-c", "--charge", dest="charge", type=int,
                        required=True,
                        help="total charge of the system")
    parser.add_argument("-t", "--taboo_tolerance", dest="taboo_tolerance",
                        type=float,
                        default=1.0,
                        help="The radius to taboo a solution (in Angstrom)")
    parser.add_argument("-r", "--ratio_taboo_particles",
                        dest="ratio_taboo_particles", type=float,
                        default=0.5,
                        help="ratio of particle within the tolerance to consider taboo current solution")
    parser.add_argument("-o", "--outputfile", dest="outputfile", type=str,
                        required=True,
                        help="the file name of the aligned conformer")
    parser.add_argument("-i", "--iterations", dest="iterations", type=int,
                        default=600,
                        help="maximum number of evaluations")
    parser.add_argument("-s", "--size", dest="size", type=int, default=15,
                        help="population size")
    parser.add_argument("-k", "--num_neighbours", dest="num_neighbours",
                        type=int, default=2,
                        help="number of neighbours")
    parser.add_argument("--force_ordered_fragment",
                        dest="force_ordered_fragment", action="store_true",
                        help="set this option to keep the fragment of the same in the order of input along the X-axis")
    parser.add_argument("--topology", dest="topology",
                        choices=["ring", "star"], type=str, default="ring",
                        help="the topology of the PSO information network")
    parser.add_argument("--initial_guess", dest="initial_guess",
                        choices=["breadth", "center", "volume"],
                        default="breadth",
                        help="where should particles should be initially put")
    parser.add_argument("--bound_setter", dest="bound_setter",
                        choices=["chain", "volume"], default="chain",
                        help="method to set the bound conditions of PSO")
    parser.add_argument("--always_write_best", dest="always_write_best",
                        action="store_true",
                        help="enable this option to output the best structure at every iteration")
    parser.add_argument("--random_seed", dest="random_seed", default=None,
                        type=int,
                        help="random seed for PSO, an integer is expected")
    parser.add_argument("--max_generations_each_conformer",
                        dest="max_generations_each_conformer", default=100,
                        type=int,
                        help="maximum generations for each conformer")
    parser.add_argument("-e", "--evaluator", dest="evaluator", type=str,
                        default="hardsphere",
                        choices=["hardsphere", "sqm"], help="Energy Evaluator")
    options = parser.parse_args()
    if options.evaluator == 'hardsphere':
        qcout_molecule = QcOutput(options.molecule)
        qcout_cation = QcOutput(options.cation)
        qcout_anion = QcOutput(options.anion)
        total_charge_cation = qcout_cation.data[0]["molecules"][-1].charge
        total_charge_anion = qcout_anion.data[0]["molecules"][-1].charge
        total_charge_mol = qcout_molecule.data[0]["molecules"][-1].charge
        num_lcm = lcm(total_charge_cation, -total_charge_anion)
        num_cation = num_lcm / total_charge_cation
        num_anion = num_lcm / -total_charge_anion
        pymatgen_mol_molecule = qcout_molecule.data[0]["molecules"][-1]
        pymatgen_mol_cation = qcout_cation.data[0]["molecules"][-1]
        pymatgen_mol_anion = qcout_anion.data[0]["molecules"][-1]
        # noinspection PyProtectedMember
        molecule = BabelMolAdaptor(pymatgen_mol_molecule)._obmol
        # noinspection PyProtectedMember
        obmol_cation = BabelMolAdaptor(pymatgen_mol_cation)._obmol
        # noinspection PyProtectedMember
        obmol_anion = BabelMolAdaptor(pymatgen_mol_anion)._obmol
        energy_evaluator = HardSphereElectrostaticEnergyEvaluator.from_qchem_output(
            qcout_molecule, qcout_cation, qcout_anion)
        fragments = [obmol_cation, obmol_anion]
    else:
        # noinspection PyProtectedMember
        molecule = BabelMolAdaptor.from_file(options.molecule,
                                             os.path.splitext(
                                                 options.molecule)[1][
                                             1:])._obmol
        fragments = []
        for frag_file in options.fragments:
            file_format = os.path.splitext(frag_file)[1][1:]
            # noinspection PyProtectedMember
            fragments.append(
                BabelMolAdaptor.from_file(frag_file, file_format)._obmol)
        energy_evaluator = SemiEmpricalQuatumMechanicalEnergyEvaluator(
            molecule, fragments, options.nums_fragments,
            total_charge=options.charge,
            taboo_tolerance_ang=options.taboo_tolerance,
            force_order_fragment=options.force_ordered_fragment,
            bound_setter=options.bound_setter)
    if len(fragments) != len(options.nums_fragments):
        raise ValueError(
            "you must specify the duplicated count for every fragment")
    placer = IonPlacer(molecule=molecule, fragments=fragments,
                       nums_fragments=options.nums_fragments,
                       energy_evaluator=energy_evaluator,
                       taboo_tolerance_ang=options.taboo_tolerance,
                       taboo_tolerance_particle_ratio=options.ratio_taboo_particles,
                       topology=options.topology,
                       initial_guess=options.initial_guess,
                       bound_setter=options.bound_setter,
                       always_write_best=options.always_write_best,
                       random_seed=options.random_seed,
                       max_generations_each_conformer=options.max_generations_each_conformer)
    energy_evaluator.arranger = placer
    placer.place(max_evaluations=options.iterations,
                 pop_size=options.size,
                 neighborhood_size=options.num_neighbours)
    print('It took {:.1f} seconds to place the salt'.format(placer
                                                            .playing_time))
Example #6
0
 def _run_qchem_on_alcf(self, log_file_object=None):
     parent_qcinp = QcInput.from_file(self.input_file)
     njobs = len(parent_qcinp.jobs)
     return_codes = []
     alcf_cmds = []
     qc_jobids = []
     for i, j in enumerate(parent_qcinp.jobs):
         qsub_cmd = copy.deepcopy(self.current_command)
         sub_input_filename = "alcf_{}_{}".format(i + 1, self.input_file)
         sub_output_filename = "alcf_{}_{}".format(i + 1, self.output_file)
         sub_log_filename = "alcf_{}_{}".format(i + 1, self.qclog_file)
         qsub_cmd[-2] = sub_input_filename
         sub_qcinp = QcInput([copy.deepcopy(j)])
         if "scf_guess" in sub_qcinp.jobs[0].params["rem"] and \
                 sub_qcinp.jobs[0].params["rem"]["scf_guess"] == "read":
             sub_qcinp.jobs[0].params["rem"].pop("scf_guess")
         if i > 0:
             if isinstance(j.mol, str) and j.mol == "read":
                 prev_qcout_filename = "alcf_{}_{}".format(
                     i + 1 - 1, self.output_file)
                 prev_qcout = QcOutput(prev_qcout_filename)
                 prev_final_mol = prev_qcout.data[0]["molecules"][-1]
                 j.mol = prev_final_mol
         sub_qcinp.write_file(sub_input_filename)
         logging.info("The command to run QChem is {}".format(
             ' '.join(qsub_cmd)))
         alcf_cmds.append(qsub_cmd)
         p = subprocess.Popen(qsub_cmd,
                              stdin=subprocess.PIPE,
                              stdout=subprocess.PIPE,
                              stderr=subprocess.PIPE)
         out, err = p.communicate()
         qc_jobid = int(out.strip())
         qc_jobids.append(qc_jobid)
         cqwait_cmd = shlex.split("cqwait {}".format(qc_jobid))
         subprocess.call(cqwait_cmd)
         output_file_name = "{}.output".format(qc_jobid)
         cobaltlog_file_name = "{}.cobaltlog".format(qc_jobid)
         with open(cobaltlog_file_name) as f:
             cobaltlog_last_line = f.readlines()[-1]
             exit_code_pattern = re.compile(
                 "an exit code of (?P<code>\d+);")
             m = exit_code_pattern.search(cobaltlog_last_line)
             if m:
                 rc = float(m.group("code"))
             else:
                 rc = -99999
             return_codes.append(rc)
         for name_change_trial in range(10):
             if not os.path.exists(output_file_name):
                 message = "{} is not found in {}, {}th wait " \
                           "for 5 mins\n".format(
                                 output_file_name,
                                 os.getcwd(), name_change_trial)
                 logging.info(message)
                 if log_file_object:
                     log_file_object.writelines([message])
                 time.sleep(60 * 5)
                 pass
             else:
                 message = "Found qchem output file {} in {}, change file " \
                           "name\n".format(output_file_name,
                                           os.getcwd(),
                                           name_change_trial)
                 logging.info(message)
                 if log_file_object:
                     log_file_object.writelines([message])
                 break
         log_file_object.flush()
         os.fsync(log_file_object.fileno())
         shutil.move(output_file_name, sub_output_filename)
         shutil.move(cobaltlog_file_name, sub_log_filename)
     overall_return_code = min(return_codes)
     with open(self.output_file, "w") as out_file_object:
         for i, job_cmd, rc, qc_jobid in zip(range(njobs), alcf_cmds,
                                             return_codes, qc_jobids):
             sub_output_filename = "alcf_{}_{}".format(
                 i + 1, self.output_file)
             sub_log_filename = "alcf_{}_{}".format(i + 1, self.qclog_file)
             with open(sub_output_filename) as sub_out_file_object:
                 header_lines = [
                     "Running Job {} of {} {}\n".format(
                         i + 1, njobs, self.input_file),
                     " ".join(job_cmd) + "\n"
                 ]
                 if i > 0:
                     header_lines = ['', ''] + header_lines
                 sub_out = sub_out_file_object.readlines()
                 out_file_object.writelines(header_lines)
                 out_file_object.writelines(sub_out)
                 if rc < 0 and rc != -99999:
                     out_file_object.writelines([
                         "Application {} exit codes: {}\n".format(
                             qc_jobid, rc), '\n', '\n'
                     ])
             if log_file_object:
                 with open(sub_log_filename) as sub_log_file_object:
                     sub_log = sub_log_file_object.readlines()
                     log_file_object.writelines(sub_log)
     return overall_return_code
Example #7
0
def main():
    import argparse
    parser = argparse.ArgumentParser(
        description="Run A QChem Job for a QChem Input File")
    parser.add_argument("-i",
                        "--input",
                        dest="input",
                        type=str,
                        required=True,
                        help="the QChem output file with imaginary frequency")
    parser.add_argument("-o",
                        "--output",
                        dest="output",
                        type=str,
                        required=True,
                        help="the QChem input file with perturbed geometry")
    parser.add_argument("-s",
                        "--scale",
                        dest="scale",
                        type=float,
                        default=0.3,
                        help="the scale factor to perturb molecule")
    parser.add_argument("-r",
                        "--reverse",
                        dest="reverse",
                        action="store_true",
                        help="use reversed direction to perturb molecule")
    parser.add_argument("-v",
                        "--verbose",
                        dest="verbose",
                        action="store_true",
                        help="print parameters")
    options = parser.parse_args()
    qcout = QcOutput(options.input)
    charge = None
    spin_multiplicity = None
    for d in qcout.data:
        j = d["input"]
        if j.charge is not None:
            charge = j.charge
        if j.spin_multiplicity is not None:
            spin_multiplicity = j.spin_multiplicity
    if qcout.data[-1]['frequencies'][0]["frequency"] < -0.00:
        old_mol = qcout.data[-1]["molecules"][-1]
        vib_mode = qcout.data[-1]['frequencies'][0]["vib_mode"]
        if options.verbose:
            if options.reverse:
                direction_text = "User forward direction"
            else:
                direction_text = "User reversed direction"
            print("{} with scale factor {}".format(direction_text,
                                                   options.scale))
        new_mol = perturb_molecule(old_mol,
                                   vib_mode,
                                   reversed_direction=options.reverse,
                                   perturb_scale=options.scale)
        qctask_freq = qcout.data[-1]["input"]
        qctask_freq.mol = "read"
        qctask_freq.charge = charge
        qctask_freq.spin_multiplicity = spin_multiplicity
        qctask_opt = copy.deepcopy(qctask_freq)
        qctask_opt.params["rem"]["jobtype"] = "opt"
        qctask_opt.params["rem"].pop("scf_guess", None)
        qctask_opt.mol = new_mol
        qcinp = QcInput([qctask_opt, qctask_freq])
        qcinp.write_file(options.output)
    else:
        raise ValueError(
            "Must have an imaginary frequency to perturb the molecule")
Example #8
0
def main():
    import argparse
    parser = argparse.ArgumentParser(
        description="Run A QChem Job for a QChem Input File")
    parser.add_argument("-f",
                        "--filename",
                        dest="filename",
                        type=str,
                        required=True,
                        help="the QChem input filename")
    parser.add_argument("-e",
                        "--eliminate",
                        dest="eli_img",
                        action="store_true",
                        help="whether to eliminate imaginary frequency")
    parser.add_argument("-b",
                        "--mixed_basis",
                        dest="mixed_basis",
                        type=json.loads,
                        required=False,
                        help="The mixed basis as a dict")
    parser.add_argument("-a",
                        "--mixed_aux_basis",
                        dest="mixed_aux_basis",
                        type=json.loads,
                        required=False,
                        help="The mixed auxiliary basis as a dict")
    parser.add_argument("-s",
                        "--solvent",
                        dest="solvent",
                        type=str,
                        required=False,
                        help="the implicit solvent")
    parser.add_argument("-n",
                        "--run_name",
                        dest="run_name",
                        type=str,
                        default=None,
                        help="the implicit solvent")
    options = parser.parse_args()
    call_qchem_task(options.filename,
                    options.solvent,
                    options.mixed_basis,
                    options.mixed_aux_basis,
                    run_name=options.run_name)
    if options.eli_img:
        base_filename = os.path.splitext(options.filename)[0]
        output_filename = base_filename + ".qcout"
        qcout = QcOutput(output_filename)
        charge = None
        spin_multiplicity = None
        for d in qcout.data:
            j = d["input"]
            if j.charge is not None:
                charge = j.charge
            if j.spin_multiplicity is not None:
                spin_multiplicity = j.spin_multiplicity
        if qcout.data[-1]['frequencies'][0]["frequency"] < -0.00:
            os.system("tar czvf img_freq_1.tar.gz *")
            old_mol = qcout.data[-1]["molecules"][-1]
            vib_mode = qcout.data[-1]['frequencies'][0]["vib_mode"]
            new_mol = perturb_molecule(old_mol, vib_mode)
            qctask_freq = qcout.data[-1]["input"]
            qctask_freq.mol = "read"
            qctask_freq.charge = charge
            qctask_freq.spin_multiplicity = spin_multiplicity
            qctask_opt = copy.deepcopy(qctask_freq)
            qctask_opt.params["rem"]["jobtype"] = "opt"
            qctask_opt.params["rem"].pop("scf_guess", None)
            qctask_opt.mol = new_mol
            qcinp = QcInput([qctask_opt, qctask_freq])
            eli_file_1 = base_filename + "_eli_img_1.qcinp"
            qcinp.write_file(eli_file_1)
            call_qchem_task(eli_file_1,
                            options.solvent,
                            options.mixed_basis,
                            options.mixed_aux_basis,
                            run_name=options.run_name)

            output_filename = base_filename + "_eli_img_1.qcout"
            qcout = QcOutput(output_filename)
            if qcout.data[-1]['frequencies'][0]["frequency"] < -0.00:
                os.system("tar czvf img_freq_2.tar.gz *")
                old_mol = qcout.data[-1]["molecules"][-1]
                vib_mode = qcout.data[-1]['frequencies'][0]["vib_mode"]
                new_mol = perturb_molecule(old_mol, vib_mode)
                qctask_freq = qcout.data[-1]["input"]
                qctask_freq.mol = "read"
                qctask_freq.charge = charge
                qctask_freq.spin_multiplicity = spin_multiplicity
                qctask_opt = copy.deepcopy(qctask_freq)
                qctask_opt.params["rem"]["jobtype"] = "opt"
                qctask_opt.params["rem"].pop("scf_guess", None)
                qctask_opt.mol = new_mol
                qcinp = QcInput([qctask_opt, qctask_freq])
                for j in qcinp.jobs:
                    j.set_dft_grid(128, 302)
                    j.set_integral_threshold(12)
                    if j.params["rem"]["jobtype"] == "opt":
                        j.scale_geom_opt_threshold(0.1, 0.1, 0.1)
                        j.set_geom_max_iterations(100)
                eli_file_2 = base_filename + "_eli_img_2.qcinp"
                qcinp.write_file(eli_file_2)
                call_qchem_task(eli_file_2,
                                options.solvent,
                                options.mixed_basis,
                                options.mixed_aux_basis,
                                run_name=options.run_name)

                output_filename = base_filename + "_eli_img_2.qcout"
                qcout = QcOutput(output_filename)
                if qcout.data[-1]['frequencies'][0]["frequency"] < -0.00:
                    os.system("tar czvf img_freq_3.tar.gz *")
                    old_mol = qcout.data[-1]["molecules"][-1]
                    vib_mode = qcout.data[-1]['frequencies'][0]["vib_mode"]
                    new_mol = perturb_molecule(old_mol, vib_mode)
                    qctask_freq = qcout.data[-1]["input"]
                    qctask_freq.mol = "read"
                    qctask_freq.charge = charge
                    qctask_freq.spin_multiplicity = spin_multiplicity
                    qctask_opt = copy.deepcopy(qctask_freq)
                    qctask_opt.params["rem"]["jobtype"] = "opt"
                    qctask_opt.params["rem"].pop("scf_guess", None)
                    qctask_opt.mol = new_mol
                    qcinp = QcInput([qctask_opt, qctask_freq])
                    for j in qcinp.jobs:
                        j.set_dft_grid(90, 590)
                        j.set_integral_threshold(12)
                        if j.params["rem"]["jobtype"] == "opt":
                            j.scale_geom_opt_threshold(0.1, 0.1, 0.1)
                            j.set_geom_max_iterations(100)
                    eli_file_3 = base_filename + "_eli_img_3.qcinp"
                    qcinp.write_file(eli_file_3)
                    call_qchem_task(eli_file_3,
                                    options.solvent,
                                    options.mixed_basis,
                                    options.mixed_aux_basis,
                                    run_name=options.run_name)
Example #9
0
    def get_task_doc(cls, path, fw_spec=None):
        """
        Get the entire task doc for a path, including any post-processing.
        """
        logger.info("Getting task doc for file:{}".format(path))
        qcout = QcOutput(zpath(path))
        data = qcout.data
        initial_mol = data[0]["molecules"][0]
        mol = data[0]["molecules"][-1]
        if data[0]["jobtype"] == "freq":
            mol = Molecule.from_dict(initial_mol.as_dict())
        bb = BabelMolAdaptor(mol)
        pbmol = bb.pybel_mol
        xyz = XYZ(mol)
        smiles = pbmol.write(str("smi")).split()[0]
        can = pbmol.write(str("can")).split()[0]
        inchi_final = pbmol.write(str("inchi")).strip()
        svg = cls.modify_svg(cls.xyz2svg(xyz))
        comp = mol.composition
        charge = mol.charge
        spin_mult = mol.spin_multiplicity
        data_dict = {}

        pga = PointGroupAnalyzer(mol)
        sch_symbol = pga.sch_symbol
        stationary_type = None
        has_structure_changing_job = False
        for d in data:
            if d["jobtype"] == "opt":
                data_dict["geom_opt"] = d
                has_structure_changing_job = True
            elif d["jobtype"] == "freq":
                data_dict["freq"] = d
                has_structure_changing_job = True
                if not d["has_error"]:
                    if d['frequencies'][0]["frequency"] < -0.00:
                        # it is stupied that -0.00 is less than 0.00
                        stationary_type = "non-minimum"
                    else:
                        stationary_type = "minimum"
                else:
                    stationary_type = "unknown"
            elif d["jobtype"] == "sp":
                suffix = "" if d["solvent_method"] == "NA" \
                    else "_" + d["solvent_method"]
                data_dict["scf" + suffix] = d
            elif d["jobtype"] == "aimd":
                data_dict["amid"] = d
                has_structure_changing_job = True

        data = data_dict

        d = {
            "path": os.path.abspath(path),
            "folder": os.path.basename(os.path.dirname(os.path.abspath(path))),
            "calculations": data,
            "molecule_initial": initial_mol.as_dict(),
            "molecule_final": mol.as_dict(),
            "pointgroup": sch_symbol,
            "pretty_formula": comp.reduced_formula,
            "reduced_cell_formula_abc": comp.alphabetical_formula,
            "formula": comp.formula,
            "charge": charge,
            "spin_multiplicity": spin_mult,
            "composition": comp.as_dict(),
            "elements": list(comp.as_dict().keys()),
            "nelements": len(comp),
            "smiles": smiles,
            "can": can,
            "inchi_final": inchi_final,
            "svg": svg,
            "xyz": str(xyz),
            "names": get_nih_names(smiles)
        }

        if stationary_type:
            d['stationary_type'] = stationary_type
        if fw_spec:
            inchi_initial = fw_spec['inchi']
            if inchi_initial != d['inchi_final']:
                d['inchi_changed'] = True
            else:
                d['inchi_changed'] = False
        if has_structure_changing_job:
            d['structure_changed'] = cls._check_structure_change(
                initial_mol, mol, path)
        else:
            d['structure_changed'] = False
        if d['structure_changed']:
            d['state'] = 'rejected'
            d['reject_reason'] = 'structural change'
        if "state" not in d:
            for v in data_dict.values():
                if v['has_error']:
                    d['state'] = "error"
                    errors = d.get("errors", [])
                    errors += v["errors"]
                    d["errors"] = errors
        if "state" not in d:
            d["state"] = "successful"

        return jsanitize(d)