def react_dots(
    params,
    dot_size1,
    dot_size2,
    prior_run_name1,
    prior_run_name2,
    random_seed=1,
    run_name_prefix="react_",
    on_queue=True,
    method="metadynamics",
):
    random_s = str(random_seed)
    random.seed(random_seed)
    run_name = run_name_prefix + str(dot_size1) + "_" + str(dot_size2)

    spoc = utils.Molecule("/fs/home/jms875/Documents/nanocrystals/pb_oleate_hydrate_morse/" + spoc_name + ".arc")

    atoms1, bonds1, angles1, dihedrals1 = cPickle.load(open("lammps/" + prior_run_name1 + ".pickle", "rb"))
    atoms1, bonds1, angles1, dihedrals1 = remove_unattached_ligands(atoms1, bonds1, angles1, dihedrals1, spoc)
    atoms2, bonds2, angles2, dihedrals2 = cPickle.load(open("lammps/" + prior_run_name2 + ".pickle", "rb"))
    atoms2, bonds2, angles2, dihedrals2 = remove_unattached_ligands(atoms2, bonds2, angles2, dihedrals2, spoc)

    atoms1_xyz = filetypes.parse_xyz(prior_run_name1 + ".xyz")
    atoms2_xyz = filetypes.parse_xyz(prior_run_name2 + ".xyz")
    goal_atoms = filetypes.parse_xyz("dot" + str(dot_size1 + dot_size2) + ".xyz")

    # set starting coordinates
    for a, b in zip(atoms1, atoms1_xyz):
        a.x = b.x
        a.y = b.y
        a.z = b.z

    for a, b in zip(atoms2, atoms2_xyz):
        a.x = b.x
        a.y = b.y
        a.z = b.z

    atoms = atoms1 + atoms2
    bonds = bonds1 + bonds2
    angles = angles1 + angles2
    dihedrals = dihedrals1 + dihedrals2
    # make sure atom types are the same in atom1 and atom2
    atom_types_by_type_index = dict([(t.type.index, t.type) for t in atoms])
    for i, a in enumerate(atoms):
        a.type = atom_types_by_type_index[a.type.index]
        a.index = i + 1
        if coul_on and a.type.index in params:
            a.charge = params[a.type.index]
        else:
            a.charge = a.type.charge

    atom_types = dict([(t.type, True) for t in atoms]).keys()
    atom_types.sort(key=lambda t: -t.element - t.index * 0.00001)

    offset = (
        radius_of_gyration([a for a in atoms1 if (a.type.notes == "PbS Nanocrystal Pb" or a.element == "S")])
        + radius_of_gyration([a for a in atoms2 if (a.type.notes == "PbS Nanocrystal Pb" or a.element == "S")])
        + 10.0
    )
    offset_vector = utils.matvec(utils.rand_rotation(), [offset, offset, offset])
    rot = utils.rand_rotation()
    for a in atoms2:  # offset by random orientation vector
        a.x, a.y, a.z = utils.matvec(rot, [a.x, a.y, a.z])
        a.x += offset_vector[0]
        a.y += offset_vector[1]
        a.z += offset_vector[2]
    box_size = [
        15.0 + max([a.x for a in atoms]) - min([a.x for a in atoms]),
        15.0 + max([a.y for a in atoms]) - min([a.y for a in atoms]),
        15.0 + max([a.z for a in atoms]) - min([a.z for a in atoms]),
    ]

    # goal_atoms = cPickle.load( open('lammps/dot'+str(dot_size1+dot_size2)+'.pickle', 'rb') )[0]

    # filetypes.write_xyz('out_meta', atoms)
    # exit()

    os.chdir("lammps")
    save_to_file(atoms, bonds, angles, dihedrals, atom_types, run_name)
    lammps.write_data_file_general(
        atoms, bonds, angles, dihedrals, box_size, run_name, atom_types=atom_types, pair_coeffs_included=False
    )
    os.system("cp ../" + sys.argv[0] + " " + run_name + ".py")

    starting_rxn_coord = radius_of_gyration(
        [a for a in atoms if (a.type.notes == "PbS Nanocrystal Pb" or a.element == "S")]
    )
    print len([a for a in atoms if (a.type.notes == "PbS Nanocrystal Pb" or a.element == "S")])
    goal_rxn_coord = radius_of_gyration(goal_atoms[0 : 2 * (dot_size1 + dot_size2)])

    colvars_file = open(run_name + ".colvars", "w")
    colvars_file.write(
        """
colvarsTrajFrequency 10000
colvarsRestartFrequency 100000

colvar {
  name gyr
  width 0.1 #size of bins, in Angstroms
  upperBoundary """
        + str(starting_rxn_coord)
        + """ #two separate dots
  lowerBoundary """
        + str(goal_rxn_coord)
        + """ #one unified dot
  lowerwallconstant 10.0
  upperwallconstant 10.0
  gyration {
	atoms {
	  atomNumbers """
        + (" ".join([str(a.index) for a in atoms if (a.type.notes == "PbS Nanocrystal Pb" or a.element == "S")]))
        + """
	}
  }
}
"""
    )
    if method.lower() == "abf":
        colvars_file.write(
            """
abf {
  colvars gyr
  hideJacobian # when using distance-based colvar: makes pmf flat at infinity
  fullSamples 1000
}
"""
        )
    elif method.lower() == "metadynamics":
        colvars_file.write(
            """
metadynamics {
  colvars gyr
  hillWeight 0.1
  newHillFrequency 1000
}
"""
        )
    else:
        raise Exception('Invalid free energy method "%s"' % method)
    colvars_file.close()

    f = open(run_name + ".in", "w")
    write_input_header(f, run_name, atom_types, read_restart=False)
    f.write(
        """
dump	1 all xyz 10000 """
        + run_name
        + """.xyz
minimize 0.0 1.0e-8 10000 1000
neigh_modify check yes every 1 delay 0
thermo_style custom pe temp
thermo 10000
restart 100000 """
        + run_name
        + """.restart1 """
        + run_name
        + """.restart2
fix motion all nve
fix implicit_solvent all langevin 400.0 400.0 100.0 """
        + random_s
        + """ zero yes gjf yes
fix		col all colvars """
        + run_name
        + """.colvars output """
        + run_name
        + """ seed """
        + random_s
        + """ tstat implicit_solvent
velocity	all create 400.0 """
        + random_s
        + """ mom yes
timestep	2.0
thermo_style	custom step temp etotal pe ke epair ebond f_col tpcpu
run """
        + str(int(1e7))
        + """
write_restart """
        + run_name
        + """.restart
"""
    )
    f.close()
    # run_job(run_name, on_queue)
    os.chdir("..")
def build_dot(N, spoc, monomer, atoms, bonds, angles, dihedrals, random_seed=1):
    random.seed(random_seed)
    pb_type = monomer.atoms[0].type
    s_type = monomer.atoms[1].type
    Q = 2.968
    L = int(math.ceil((2 * N) ** 0.333))
    center = utils.Struct(x=0.0, y=0.0, z=0.0)
    core_atoms = []
    if L * Q < 30:  # octahedron
        L += 6
        for zi in range(L):
            # L2 = int(( math.sqrt(3)*( L/2-abs(zi-L/2) ) ))
            L2 = (L / 2 - abs(zi - L / 2)) * 2
            for xi in range(L2):
                for yi in range(L2):
                    x = (xi - L2 * 0.5 + 0.5) * Q
                    y = (yi - L2 * 0.5 + 0.5) * Q
                    z = (zi - L * 0.5 + 0.5) * Q
                    core_atoms.append(
                        utils.Struct(
                            x=x,
                            y=y,
                            z=z,
                            element="Pb" if (xi + yi + zi) % 2 else "S",
                            type=pb_type if (xi + yi + zi) % 2 else s_type,
                        )
                    )
    else:  # cube
        L += 2
        for xi in range(L):
            for yi in range(L):
                for zi in range(L):
                    x = (xi - L * 0.5 + 0.5) * Q
                    y = (yi - L * 0.5 + 0.5) * Q
                    z = (zi - L * 0.5 + 0.5) * Q
                    core_atoms.append(
                        utils.Struct(
                            x=x,
                            y=y,
                            z=z,
                            element="Pb" if (xi + yi + zi) % 2 else "S",
                            type=pb_type if (xi + yi + zi) % 2 else s_type,
                        )
                    )

                # filetypes.write_xyz('out', core_atoms)
                # sys.exit()

    for a in core_atoms:
        a.dist = dist = utils.dist(a, center)
        a.neighbors = []
        for b in core_atoms:
            if a is not b and utils.dist_squared(a, b) < Q ** 2 + 0.1:
                a.neighbors.append(b)

    core_atoms.sort(key=lambda a: a.dist)
    s_atoms = [a for a in core_atoms if a.element == "S"][:N]
    pb_atoms = {}
    for s in s_atoms:
        for a in s.neighbors:
            if a.type is pb_type:
                pb_atoms[a] = True

    pb_atoms = pb_atoms.keys()
    pb_atoms.sort(key=lambda a: a.dist)
    # excess_pb_atoms = pb_atoms[N:] #tends to pick all from one side

    closest_dist_to_remove = pb_atoms[N].dist

    core_pb_atoms = [a for a in pb_atoms if a.dist < closest_dist_to_remove - 0.01]
    marginal_pb_atoms = [
        a for a in pb_atoms if (abs(a.dist - closest_dist_to_remove) < 0.01 and a not in core_pb_atoms)
    ]
    random.shuffle(marginal_pb_atoms)
    core_pb_atoms = core_pb_atoms + marginal_pb_atoms[: N - len(core_pb_atoms)]
    excess_pb_atoms = [a for a in pb_atoms if a not in core_pb_atoms]

    for a in s_atoms + core_pb_atoms:
        atoms.append(a)

    import numpy

    def add_spoc_at_pb(pb):
        spoc.add_to(0.0, 0.0, 0.0, atoms, bonds, angles, dihedrals)

        direction = [pb.x, pb.y, pb.z]
        direction /= numpy.linalg.norm(direction)

        dot = numpy.dot(direction, [1.0, 0.0, 0.0])
        theta = math.acos(dot)
        cross = numpy.cross(direction, [1.0, 0.0, 0.0])
        cross /= -numpy.linalg.norm(cross)
        w = numpy.array(cross)

        for a in atoms[-len(spoc.atoms) :]:
            v = numpy.array([a.x, a.y, a.z])
            a.x, a.y, a.z = (
                v * math.cos(theta) + numpy.cross(w, v) * math.sin(theta) + w * numpy.dot(w, v) * (1 - math.cos(theta))
            )  # Rodrigues' rotation formula
            a.x += pb.x
            a.y += pb.y
            a.z += pb.z

    for pb in excess_pb_atoms:
        add_spoc_at_pb(pb)

        # add more Pb complexes as needed

    new_pb = 0
    while new_pb < N + 3:
        x, y, z = [Q * (3 + (2 * N) ** 0.333) / 2] * 3
        M = utils.rand_rotation()
        x, y, z = utils.matvec(M, [x, y, z])
        pb = utils.Struct(x=x, y=y, z=z, element="Pb", type=pb_type)
        too_close = False
        for a in atoms:
            if utils.dist_squared(a, pb) < (Q + 2) ** 2:
                too_close = True
                break
        if not too_close:
            add_spoc_at_pb(pb)
            new_pb += 1