diff --git a/Dockerfile b/Dockerfile index 5af253d2db..b6a34b22a1 100644 --- a/Dockerfile +++ b/Dockerfile @@ -78,19 +78,13 @@ COPY . /code/timemachine/ WORKDIR /code/timemachine/ RUN pip install --no-cache-dir -e . && rm -rf ./build -# Container with only cuda base, half the size of the timemachine_cuda_dev container -# Need to copy curand/cudart as these are dependencies of the Timemachine GPU code -FROM docker.io/nvidia/cuda:12.4.1-base-ubuntu20.04 AS timemachine +FROM docker.io/nvidia/cuda:12.4.1-devel-ubuntu20.04 AS timemachine ARG LIBXRENDER_VERSION ARG LIBXEXT_VERSION RUN (apt-get update || true) && apt-get install --no-install-recommends -y libxrender1=${LIBXRENDER_VERSION} libxext-dev=${LIBXEXT_VERSION} \ && apt-get clean \ && rm -rf /var/lib/apt/lists/* -# Copy curand libraries from image, only require cudart and curand -COPY --from=timemachine_cuda_dev /usr/local/cuda/targets/x86_64-linux/lib/libcurand* /usr/local/cuda/targets/x86_64-linux/lib/ -COPY --from=timemachine_cuda_dev /usr/local/cuda/lib64/libcurand* /usr/local/cuda/lib64/ - COPY --from=timemachine_cuda_dev /opt/conda/ /opt/conda/ COPY --from=timemachine_cuda_dev /code/ /code/ COPY --from=timemachine_cuda_dev /root/.bashrc /root/.bashrc diff --git a/environment.yml b/environment.yml index c95fe148d0..2cc1d72880 100644 --- a/environment.yml +++ b/environment.yml @@ -8,3 +8,5 @@ dependencies: # Include numpy/scipy to reduce the size of the Docker container - numpy=2.2.2 - scipy=1.15.1 + - openff-toolkit=0.16.9 + - openff-recharge=0.5.3 diff --git a/requirements.txt b/requirements.txt index 88e8d598ac..9edb424dee 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,7 @@ rdkit==2024.9.4 --extra-index-url https://pypi.anaconda.org/OpenEye/simple openeye-toolkits==2020.2.0 openmm==8.2.0 +gpu4pyscf-cuda12x==1.4.3 +cutensor-cu12==2.2.0 +Auto3D==2.3.1 +torch==2.7.0 diff --git a/tests/test_handlers.py b/tests/test_handlers.py index 0d3bd9556a..ba39cf12e7 100644 --- a/tests/test_handlers.py +++ b/tests/test_handlers.py @@ -23,8 +23,6 @@ from timemachine.testsystems import fetch_freesolv from timemachine.utils import path_to_internal_file -pytestmark = [pytest.mark.nocuda] - TEST_FEATURE_SIZE = 16 @@ -123,6 +121,7 @@ def env_nn_args(): return enc_unflatten_str, params, props +@pytest.mark.nocuda def test_harmonic_bond(): patterns = [ ["[#6X4:1]-[#6X4:2]", 0.1, 0.2], @@ -250,6 +249,7 @@ def test_harmonic_bond(): assert bond_params.shape == (0, 2) +@pytest.mark.nocuda def test_harmonic_angle(): patterns = [ ["[*:1]-[#8:2]-[*:3]", 0.1, 0.2], @@ -273,6 +273,7 @@ def test_harmonic_angle(): assert angle_params.shape == (0, 3) +@pytest.mark.nocuda def test_proper_torsion(): # proper torsions have a variadic number of terms @@ -323,6 +324,7 @@ def test_proper_torsion(): assert proper_idxs.shape == (0, 4) +@pytest.mark.nocuda def test_improper_torsion(): patterns = [ ["[*:1]~[#6X3:2](~[*:3])~[*:4]", 1.5341333333333333, 3.141592653589793, 2.0], @@ -378,6 +380,7 @@ def test_improper_torsion(): assert idxs[0] < idxs[-1] +@pytest.mark.nocuda def test_exclusions(): mol = Chem.MolFromSmiles("FC(F)=C(F)F") exc_idxs, scales = nonbonded.generate_exclusion_idxs(mol, scale12=0.0, scale13=0.2, scale14_q=0.25, scale14_lj=0.75) @@ -428,6 +431,7 @@ def test_exclusions(): np.testing.assert_equal(scales, expected_scales) +@pytest.mark.nocuda def test_am1bcc_parameterization(): # currently takes no parameters smirks = [] @@ -456,6 +460,30 @@ def test_am1bcc_parameterization(): # assert vjp_fn(charges_adjoints) == None +def test_resp_parameterization(): + # currently takes no parameters + smirks = [] + params = [] + props = None + + cache_key = nonbonded.RESP_CHARGE_CACHE + resp = nonbonded.RESPHandler(smirks, params, props) + mol = Chem.AddHs(Chem.MolFromSmiles("C1CNCOC1F")) + + assert not mol.HasProp(cache_key) + + charges = resp.parameterize(mol) + + assert len(charges) == mol.GetNumAtoms() + assert mol.HasProp(cache_key) + + cached_charges = resp.parameterize(mol) + np.testing.assert_equal(charges, cached_charges) + + new_charges, vjp_fn = jax.vjp(functools.partial(resp.partial_parameterize, None, mol)) + + +@pytest.mark.nocuda def test_am1_ccc(): patterns = [ ["[#6X4:1]-[#1:2]", 0.46323257920556493], @@ -564,6 +592,7 @@ def test_am1_ccc(): np.testing.assert_array_equal(es_params_from_cache, es_params) +@pytest.mark.nocuda def test_simple_charge_handler(): patterns = [ ["[#1:1]", 99.0], @@ -679,6 +708,7 @@ def test_gbsa_handler(): assert np.all(adjoints[mask] == 0.0) +@pytest.mark.nocuda def test_am1ccc_throws_error_on_phosphorus(): """Temporary, until phosphorus patterns are added to AM1CCC port""" ff = Forcefield.load_default() @@ -692,6 +722,7 @@ def test_am1ccc_throws_error_on_phosphorus(): assert "unsupported element" in str(e) +@pytest.mark.nocuda @pytest.mark.parametrize( "am1bcc_ff", ["smirnoff_1_1_0_am1bcc.py", "smirnoff_2_0_0_am1bcc.py", "smirnoff_2_2_0_am1bcc.py"] ) @@ -717,6 +748,7 @@ def test_am1bcc_handles_phosphorus(am1bcc_ff): ) +@pytest.mark.nocuda def test_am1_differences(): ff = Forcefield.load_default() @@ -754,6 +786,7 @@ def test_am1_differences(): assert 0 +@pytest.mark.nocuda def test_am1elf10_conformer_independence(): with path_to_internal_file("timemachine.testsystems.data", "ligands_40.sdf") as path_to_ligand: mols = utils.read_sdf(path_to_ligand) @@ -780,6 +813,7 @@ def test_am1elf10_conformer_independence(): assert np.sum(delta_charges) == pytest.approx(0.0) +@pytest.mark.nocuda def test_trans_carboxlic_acid(): # Test fallback to turn off hydrogen sampling if charge generation failed # due to trans-COOH @@ -795,6 +829,7 @@ def test_trans_carboxlic_acid(): assert np.sum(delta_charges) == pytest.approx(0.0) +@pytest.mark.nocuda def test_freesolv_failures(): # Test failures for 3 cases in freesolv in openeye toolkits 2022.2.2 with path_to_internal_file("timemachine.testsystems.data", "freesolv_omega_failures.sdf") as path_to_ligand: @@ -805,6 +840,7 @@ def test_freesolv_failures(): assert am1elf10_charges is not None +@pytest.mark.nocuda def test_compute_or_load_oe_charges(): """Loop over test ligands, asserting that charges are stored in expected property and that the same charges are returned on repeated calls""" @@ -844,6 +880,7 @@ def assert_permutation_equivariance(mol, fxn, perm): np.testing.assert_allclose(qs_perm, qs[perm]) +@pytest.mark.nocuda @pytest.mark.parametrize("mol_idx", [0, 1, 2, 3, 4, 5]) def test_partial_charge_equivariance_on_freesolv(mol_idx): ff = Forcefield.load_default() @@ -857,6 +894,7 @@ def test_partial_charge_equivariance_on_freesolv(mol_idx): assert_permutation_equivariance(mol, ff.q_handle.parameterize, perm) +@pytest.mark.nocuda def test_charging_compounds_with_non_zero_charge(): patterns = [ ["[#6a:1]:[#6a:2]", 0.0], @@ -884,6 +922,7 @@ def test_charging_compounds_with_non_zero_charge(): np.testing.assert_almost_equal(np.sum(es_params) / np.sqrt(ONE_4PI_EPS0), -1.0, decimal=5) +@pytest.mark.nocuda def test_precomputed_charge_handler(): with path_to_internal_file("timemachine.testsystems.water_exchange", "bb_centered_espaloma.sdf") as path_to_ligand: mol = utils.read_sdf(path_to_ligand)[0] @@ -995,6 +1034,7 @@ def test_precomputed_charge_handler(): ) +@pytest.mark.nocuda def test_compute_or_load_bond_smirks_matches(): """Loop over test ligands, asserting that * verify no cache key @@ -1038,6 +1078,7 @@ def test_compute_or_load_bond_smirks_matches(): np.testing.assert_array_equal(fresh_types, cached_types) +@pytest.mark.nocuda def test_apply_bond_charge_corrections(): """Assert that applying random bond charge corrections does not change net charge""" @@ -1067,6 +1108,7 @@ def test_apply_bond_charge_corrections(): np.testing.assert_almost_equal(final_net_charge, initial_net_charge) +@pytest.mark.nocuda def test_lennard_jones_handler(): patterns = [ ["[#1:1]", 99.0, 999.0], @@ -1144,6 +1186,7 @@ def test_lennard_jones_handler(): assert np.all(adjoints[mask] == 0.0) +@pytest.mark.nocuda def test_symmetric_am1ccc(): """Assert that (symmetric_bond_smarts, +1.0) has same behavior as (symmetric_bond_smarts, 0.0) on one test mol""" @@ -1164,6 +1207,7 @@ def test_symmetric_am1ccc(): np.testing.assert_array_equal(test_charges, ref_charges) +@pytest.mark.nocuda def test_nn_handler(): with path_to_internal_file("timemachine.testsystems.data", "ligands_40.sdf") as path_to_ligand: all_mols = utils.read_sdf(path_to_ligand) @@ -1196,6 +1240,7 @@ def loss_fn(params): print("jit grad", jax.jit(grad_fn)(params)) # also a few seconds +@pytest.mark.nocuda def test_harmonic_bonds_complete(): """On a test molecule containing [oxygen] ~ [halogen] bonds, assert that a ValueError is raised.""" @@ -1210,6 +1255,7 @@ def test_harmonic_bonds_complete(): assert "missing bonds" in str(e) +@pytest.mark.nocuda @pytest.mark.parametrize("is_nn", [True, False]) @pytest.mark.parametrize( "protein_path_and_symmetries", @@ -1312,6 +1358,7 @@ def test_env_bcc_peptide_symmetries(protein_path_and_symmetries, is_nn, env_nn_a np.testing.assert_almost_equal(raw_charges[other], raw_charges[first]) +@pytest.mark.nocuda @pytest.mark.nightly(reason="Slow") @pytest.mark.parametrize("is_nn", [True, False]) @pytest.mark.parametrize("protein_path", ["5dfr_solv_equil.pdb", "hif2a_nowater_min.pdb"]) diff --git a/tests/test_serialization.py b/tests/test_serialization.py index a7524947bd..707e0c9c3f 100644 --- a/tests/test_serialization.py +++ b/tests/test_serialization.py @@ -281,6 +281,21 @@ def test_am1bcc(): assert am1.props == am1.props +def test_resp(): + smirks = [] + params = [] + props = None + + resp = nonbonded.RESPHandler(smirks, params, props) + obj = resp.serialize() + all_handlers, _, _ = deserialize_handlers(bin_to_str(obj)) + + resp = all_handlers[0] + np.testing.assert_equal(resp.smirks, resp.smirks) + np.testing.assert_equal(resp.params, resp.params) + assert resp.props == resp.props + + def test_am1ccc(): patterns = [ ["[#6X4:1]-[#1:2]", 0.46323257920556493], diff --git a/timemachine/ff/__init__.py b/timemachine/ff/__init__.py index ab0304f3ef..1e034e326f 100644 --- a/timemachine/ff/__init__.py +++ b/timemachine/ff/__init__.py @@ -58,6 +58,7 @@ class Forcefield: nonbonded.AM1BCCCCCHandler, nonbonded.PrecomputedChargeHandler, nonbonded.NNHandler, + nonbonded.RESPHandler, ] ] q_handle_intra: Optional[ @@ -68,6 +69,7 @@ class Forcefield: nonbonded.AM1BCCCCCIntraHandler, nonbonded.PrecomputedChargeHandler, nonbonded.NNHandler, + nonbonded.RESPIntraHandler, ] ] @@ -201,6 +203,7 @@ def from_handlers( nonbonded.AM1CCCIntraHandler, nonbonded.AM1BCCIntraHandler, nonbonded.SimpleChargeIntraHandler, + nonbonded.RESPIntraHandler, nonbonded.PrecomputedChargeIntraHandler, ), ): @@ -220,6 +223,7 @@ def from_handlers( nonbonded.AM1BCCCCCHandler, nonbonded.AM1CCCHandler, nonbonded.AM1BCCHandler, + nonbonded.RESPHandler, nonbonded.SimpleChargeHandler, nonbonded.PrecomputedChargeHandler, nonbonded.NNHandler, @@ -258,6 +262,8 @@ def from_handlers( q_handle_intra = nonbonded.PrecomputedChargeIntraHandler( q_handle.smirks, q_handle.params, q_handle.props ) + elif isinstance(q_handle, nonbonded.RESPHandler): + q_handle_intra = nonbonded.RESPIntraHandler(q_handle.smirks, q_handle.params, q_handle.props) else: raise ValueError(f"Unsupported charge handler {q_handle}") diff --git a/timemachine/ff/handlers/nonbonded.py b/timemachine/ff/handlers/nonbonded.py index fd52d700ad..8aa7876044 100644 --- a/timemachine/ff/handlers/nonbonded.py +++ b/timemachine/ff/handlers/nonbonded.py @@ -1,7 +1,12 @@ import base64 +import os import pickle +import subprocess +import tempfile import warnings -from collections import Counter +from collections import Counter, defaultdict +from functools import partial +from shutil import which import jax.numpy as jnp import networkx as nx @@ -9,6 +14,9 @@ from jax import jit, vmap from numpy.typing import NDArray from rdkit import Chem +from rdkit.Chem import AllChem +from rdkit.Chem.Descriptors import NumRadicalElectrons +from rdkit.Chem.rdMolTransforms import GetBondLength from timemachine import constants from timemachine.ff.handlers.bcc_aromaticity import AromaticityModel @@ -21,7 +29,9 @@ make_residue_mol_from_template, update_mol_topology, ) -from timemachine.ff.handlers.utils import match_smirks as rd_match_smirks +from timemachine.ff.handlers.utils import ( + match_smirks as rd_match_smirks, +) from timemachine.graph_utils import convert_to_nx CACHE_SUFFIX = "Cache" @@ -29,6 +39,7 @@ AM1BCC_CHARGE_CACHE = "AM1BCCCache" AM1ELF10_CHARGE_CACHE = "AM1ELF10Cache" AM1BCCELF10_CHARGE_CACHE = "AM1BCCELF10Cache" +RESP_CHARGE_CACHE = "RESPCache" BOND_SMIRK_MATCH_CACHE = "BondSmirkMatchCache" NN_FEATURES_PROPNAME = "NNFeatures" @@ -37,6 +48,7 @@ AM1BCC = "AM1BCC" AM1BCCELF10 = "AM1BCCELF10" ELF10_MODELS = (AM1ELF10, AM1BCCELF10) +RESP = "RESP" def convert_to_oe(mol): @@ -119,6 +131,7 @@ def oe_assign_charges(mol, charge_model=AM1BCCELF10): if charge_model in ELF10_MODELS: oe_generate_conformations(oemol) + print(f"Generated {oemol.NumConfs()} OpenEye conformers") result = oequacpac.OEAssignCharges(oemol, charge_engine) if result is False: @@ -150,6 +163,313 @@ def oe_assign_charges(mol, charge_model=AM1BCCELF10): return inlined_constant * partial_charges[inv_permutation] +def rdkit_generate_conformations(mol): + mol_copy = Chem.Mol(mol) + ri = mol.GetRingInfo() + largest_ring_size = max((len(r) for r in ri.AtomRings()), default=0) + if largest_ring_size > 10: + print("Detected macrocycle") + params = Chem.rdDistGeom.ETKDGv3() + else: + params = Chem.rdDistGeom.srETKDGv3() + params.useSmallRingTorsions = True + + params.pruneRmsThresh = 1.0 + params.clearConfs = True + + confs = len(AllChem.EmbedMultipleConfs(mol, 800, params)) + if confs == 0: + mol.AddConformer(mol_copy.GetConformer()) + AllChem.MMFFOptimizeMoleculeConfs(mol, maxIters=500) + + +def make_xyz(rdmol: Chem.Mol) -> str: + xyz = [] + for atom in rdmol.GetAtoms(): + atom_index = atom.GetIdx() + pos = rdmol.GetConformer().GetAtomPosition(atom_index) + xyz.append(f"{atom.GetSymbol()} {pos.x} {pos.y} {pos.z}") + return "\n".join(xyz) + + +def resp_assign_partial_charges(_rdmol: Chem.Mol, use_conformers: list) -> tuple[np.ndarray, float]: + """ + Calculate RESP (Restrained ElectroStatic Potential) partial charges for a molecule. + + This function performs quantum mechanical calculations to derive atomic partial charges + that best reproduce the molecular electrostatic potential while applying restraints + to prevent overfitting. + + Parameters + ---------- + _rdmol : Chem.Mol + Input RDKit molecule object + use_conformers : list[Quantity] + List of conformer coordinates to use (only the first conformer is used) + + Returns + ------- + tuple[np.ndarray, float] + - Array of RESP partial charges for each atom + - Total DFT energy of the molecule + """ + from Auto3D.ASE.geometry import opt_geometry + from gpu4pyscf.pop import esp + from pyscf import gto, scf + from pyscf.data import radii + + # Check that antechamber is available for symmetry checking + ANTECHAMBER_PATH = which("antechamber") + if ANTECHAMBER_PATH is None: + raise ValueError("Antechamber not found, cannot run assign_partial_charges()") + + # Create a copy of the molecule and set conformer positions + rdmol = Chem.Mol(_rdmol) + rdmol.RemoveAllConformers() + conf = Chem.Conformer(rdmol.GetNumAtoms()) + conf.SetPositions(np.array(use_conformers[0], dtype=np.float64)) + rdmol.AddConformer(conf, assignId=True) + + # Compute charges + with tempfile.TemporaryDirectory() as tmpdir: + # Get the formal charge of the molecule for QM calculations + net_charge = Chem.GetFormalCharge(rdmol) + + # Write molecule to SDF file for antechamber processing + print(Chem.MolToMolBlock(rdmol), file=open(os.path.join(tmpdir, "molecule.sdf"), "w+")) + + # Run antechamber to generate atom types and connectivity information + # This creates a .ac file with atom type assignments needed for RESP + subprocess.check_output( + [ + "antechamber", + "-i", + "molecule.sdf", + "-o", + "charged.ac", + "-fo", + "ac", + "-fi", + "sdf", + "-nc", + str(net_charge), + ], + cwd=tmpdir, + ) + + # Attempt to optimize geometry using AIMNET neural network potential + # This can provide better initial coordinates for QM calculations + try: + out_path = opt_geometry(os.path.join(tmpdir, "molecule.sdf"), model_name="AIMNET") + m = Chem.MolFromMolFile(out_path, removeHs=False) + bad_bond = False + # Check for unreasonable bond lengths that indicate optimization failure + for bnd in m.GetBonds(): + bond_length = GetBondLength(m.GetConformer(), bnd.GetBeginAtomIdx(), bnd.GetEndAtomIdx()) + if bond_length > 3.0: + print( + f"Bond length {bond_length} is too high between {bnd.GetBeginAtom().GetSymbol()} and {bnd.GetEndAtom().GetSymbol()}" + ) + bad_bond = True + + if not bad_bond: + # Use optimized coordinates if geometry is reasonable + xyz = make_xyz(m) + else: + # Fall back to original coordinates if optimization failed + xyz = make_xyz(rdmol) + except Exception as e: + # If optimization fails entirely, use original coordinates + print(e) + xyz = make_xyz(rdmol) + + # Generate RESP input file with symmetry constraints + # respgen identifies chemically equivalent atoms that should have equal charges + subprocess.check_output(["respgen", "-i", "charged.ac", "-o", "tmp.respin", "-f", "resp"], cwd=tmpdir) + + # Parse symmetry constraints from respgen output + # These ensure chemically equivalent atoms (e.g., methyl hydrogens) get identical charges + symmetry_groups = defaultdict(set) + with open(os.path.join(tmpdir, "tmp.respin")) as f: + lines = f.readlines() + for i, line in enumerate(lines): + if "&end" in line: + # Parse atom symmetry groups starting 4 lines after "&end" + for atm_idx, l in enumerate(lines[i + 4 :], 1): + parts = l.split() + if parts: + group = int(parts[1]) + if group: + # Add atoms to their symmetry group + symmetry_groups[group].add(atm_idx - 1) + symmetry_groups[group].add(group - 1) + + # Set up PySCF molecule object for quantum calculations + mol = gto.Mole() + mol.cart = True # Use Cartesian basis functions + mol.charge = Chem.GetFormalCharge(rdmol) + mol.spin = NumRadicalElectrons(rdmol) # Number of unpaired electrons + mol.atom = xyz # Atomic coordinates + mol.basis = "6-31gs" # Standard basis set for RESP calculations + mol.build() + + # Perform Restricted Hartree-Fock calculation on GPU + mf = scf.RHF(mol).to_gpu() + e_dft = mf.kernel() # Total electronic energy + dm = mf.make_rdm1() # Density matrix + + # Define van der Waals radii for ESP grid point generation + # These determine where electrostatic potential points are sampled + # Taken from https://github.com/pyscf/gpu4pyscf/blob/57cf1d437adb820ce7f69f8872f2500c751bdd97/gpu4pyscf/pop/esp.py#L32 + # with an added parameter for Br + rad = ( + 1.0 + / radii.BOHR + * np.asarray( + [ + -1, + 1.20, # H + 1.20, # He + 1.37, # Li + 1.45, # Be + 1.45, # B + 1.50, # C + 1.50, # N, + 1.40, # O + 1.35, # F, + 1.30, # Ne, + 1.57, # Na, + 1.36, # Mg + 1.24, # Al, + 1.17, # Si, + 1.80, # P, + 1.75, # S, + 1.70, # Cl + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + -1, + 1.85, # Br + ] + ) + ) + + # First fit ESP charges without restraints + # This provides initial charges that exactly reproduce the electrostatic potential + esp.esp_solve(mol, dm, rad=rad) + print("Fitted ESP charge") + + # First stage RESP fitting with default restraints + # This applies weak restraints to prevent overfitting + esp.resp_solve(mol, dm, rad=rad) + sum_constraints = [] + + # Second stage RESP fitting with symmetry constraints + # This ensures chemically equivalent atoms have identical charges + rows = esp.resp_solve( + mol, + dm, + rad=rad, + resp_a=1e-3, # Restraint strength parameter + sum_constraints=sum_constraints, + equal_constraints=[list(s) for s in symmetry_groups.values()], # Symmetry constraints + ).tolist() + + return rows, e_dft + + +def resp_assign_elf_charges(_rdmol): + """ + Calculate RESP charges using ELF (Electrostatically Least-interacting Functional) conformer selection. + + This function generates multiple conformers, selects the most diverse ones using ELF, + computes RESP charges for each conformer, and returns the averaged charges. + + Parameters + ---------- + _rdmol : Chem.Mol + Input RDKit molecule object + + Returns + ------- + np.array + Array of averaged RESP partial charges scaled by sqrt(ONE_4PI_EPS0) + """ + # Create a copy of the molecule for conformer generation + + from openff.recharge.utilities.molecule import extract_conformers + from openff.toolkit import RDKitToolkitWrapper, unit + from openff.toolkit.topology import Molecule + + rdmol = Chem.Mol(_rdmol) + rdkit_generate_conformations(rdmol) + + print(f"Generated {rdmol.GetNumConformers()} RDKit conformers") + + # Convert to OpenFF Molecule for ELF conformer selection + molecule: Molecule = Molecule.from_rdkit(rdmol, allow_undefined_stereo=True) + # Apply ELF conformer selection to get diverse, representative conformers + # This helps ensure charges are computed from a representative ensemble + molecule.apply_elf_conformer_selection( + limit=10, toolkit_registry=RDKitToolkitWrapper(), rms_tolerance=1.0 * unit.angstrom + ) + + print(f"Selected {len(molecule.conformers)} RDKit conformers") + + # Extract conformer coordinates for RESP calculations + conformers = extract_conformers(molecule) + + # Calculate RESP charges for each selected conformer + charges = [] + energies = [] + for chs, energy in map(partial(resp_assign_partial_charges, _rdmol), [[c] for c in conformers]): + if chs is not None: + charges.append(chs) + energies.append(energy) + + # Convert charges list to numpy array for averaging + am1_partial_charges = np.array(charges) + + # Average charges across all conformers + # This provides more robust charges that account for conformational flexibility + partial_charges = np.mean(am1_partial_charges, axis=0) + + # Ensure total charge equals the formal charge of the molecule + # Small numerical errors can accumulate, so we redistribute any discrepancy + expected_charge = Chem.GetFormalCharge(rdmol) + current_charge = 0.0 + for pc in partial_charges: + current_charge += pc + charge_offset = (expected_charge - current_charge) / rdmol.GetNumAtoms() + partial_charges += charge_offset + + # Verify that the charges sum up to an integer + net_charge = np.sum(partial_charges) + net_charge_is_integral = np.isclose(net_charge, np.round(net_charge), atol=1e-5) + assert net_charge_is_integral, f"Charge is not an integer: {net_charge}" + + # Apply scaling factor for TimeMachine's optimized charge representation + # https://github.com/proteneer/timemachine#forcefield-gotchas + # "The charges have been multiplied by sqrt(ONE_4PI_EPS0) as an optimization." + inlined_constant = np.sqrt(constants.ONE_4PI_EPS0) + + # returned charges are in TM units, in original atom ordering + return inlined_constant * partial_charges + + def generate_exclusion_idxs( mol: Chem.Mol, scale12: float, scale13: float, scale14_lj: float, scale14_q: float ) -> tuple[NDArray, NDArray]: @@ -261,6 +581,25 @@ def compute_or_load_oe_charges(mol, mode=AM1ELF10): return np.array(oe_charges) +def compute_or_load_resp_charges(mol): + """ + Unless already cached in mol's "{mode}{CACHE_SUFFIX}" property, + use RESP to compute partial charges. + """ + + mode = "RESP" + # check for cache + cache_prop_name = f"{mode}{CACHE_SUFFIX}" + if not mol.HasProp(cache_prop_name): + resp_charges = list(resp_assign_elf_charges(mol)) + mol.SetProp(cache_prop_name, base64.b64encode(pickle.dumps(resp_charges))) + else: + resp_charges = pickle.loads(base64.b64decode(mol.GetProp(cache_prop_name))) + assert len(resp_charges) == mol.GetNumAtoms(), "Charge cache has different number of charges than mol atoms" + + return np.array(resp_charges) + + def compute_or_load_bond_smirks_matches(mol, smirks_list): """Unless already cached in mol's "BondSmirkMatchCache" property, uses OpenEye to compute arrays of ordered bonds and their assigned types. @@ -616,10 +955,44 @@ def static_parameterize(mol): return compute_or_load_oe_charges(mol, mode=AM1BCCELF10) +class RESPHandler(SerializableMixIn): + """The RESPHandler generates charges for molecules using RESP.""" + + def __init__(self, smirks, params, props): + assert len(smirks) == 0 + assert len(params) == 0 + assert props is None + self.smirks = [] + self.params = [] + self.props = None + + def partial_parameterize(self, _, mol): + return self.static_parameterize(mol) + + def parameterize(self, mol): + return self.static_parameterize(mol) + + @staticmethod + def static_parameterize(mol): + """ + Parameters + ---------- + + mol: Chem.ROMol + molecule to be parameterized. + + """ + return compute_or_load_resp_charges(mol) + + class AM1BCCIntraHandler(AM1BCCHandler): pass +class RESPIntraHandler(RESPHandler): + pass + + class AM1BCCSolventHandler(AM1BCCHandler): pass diff --git a/timemachine/ff/params/smirnoff_2_0_0_resp.py b/timemachine/ff/params/smirnoff_2_0_0_resp.py new file mode 100644 index 0000000000..3cf7f2ea42 --- /dev/null +++ b/timemachine/ff/params/smirnoff_2_0_0_resp.py @@ -0,0 +1,341 @@ +{ 'RESP': {'patterns': []}, + 'HarmonicAngle': { 'patterns': [ ['[*:1]~[#6X4:2]-[*:3]', 445.22208650928565, 2.034139115548445], + ['[#1:1]-[#6X4:2]-[#1:3]', 408.161690475075, 2.017654719697188], + ['[*;r3:1]1~;@[*;r3:2]~;@[*;r3:3]1', 1046.343507881973, 1.2917422253232633], + ['[*;r3:1]~;@[*;r3:2]~;!@[*:3]', 247.73547057550505, 2.075101872321204], + ['[*:1]~;!@[*;r3:2]~;!@[*:3]', 695.2915994078112, 2.028607541261588], + ['[#1:1]-[*;r3:2]~;!@[*:3]', 308.2444922396122, 1.989244072621022], + ['[#6r4:1]-;@[#6r4:2]-;@[#6r4:3]', 322.1710130642956, 2.1323333003734444], + ['[!#1:1]-[#6r4:2]-;!@[!#1:3]', 435.140329714076, 2.1028500799345933], + ['[!#1:1]-[#6r4:2]-;!@[#1:3]', 378.06397343272533, 2.0794311539961896], + ['[*:1]~[#6X3:2]~[*:3]', 484.4928854933472, 2.213186435767863], + ['[#1:1]-[#6X3:2]~[*:3]', 285.07077453214373, 2.254679037277145], + ['[#1:1]-[#6X3:2]-[#1:3]', 189.92276762635456, 2.2816855726476373], + ['[*;r6:1]~;@[*;r5:2]~;@[*;r5;x2:3]', 119.85170083943233, 2.414112139022697], + ['[*:1]~;!@[*;X3;r5:2]~;@[*;r5:3]', 331.60808200535575, 2.2654850246196965], + ['[#8X1:1]~[#6X3:2]~[#8:3]', 1685.4135433330089, 2.2247562159442307], + ['[*:1]~[#6X2:2]~[*:3]', 280.91222140604845, 3.141592653589793], + ['[*:1]~[#7X2:2]~[*:3]', 361.92897911435153, 3.141592653589793], + ['[*:1]-[#7X4,#7X3,#7X2-1:2]-[*:3]', 410.1658045477351, 2.004361594786895], + ['[#1:1]-[#7X4,#7X3,#7X2-1:2]-[*:3]', 791.028080064996, 1.9359230395787466], + ['[*:1]~[#7X3$(*~[#6X3,#6X2,#7X2+0]):2]~[*:3]', 463.76980237685206, 2.048083786426299], + ['[#1:1]-[#7X3$(*~[#6X3,#6X2,#7X2+0]):2]-[*:3]', 530.9012827843272, 2.0159797763293597], + ['[*:1]~[#7X2+0:2]~[*:3]', 460.89961161789523, 1.9699775782320603], + ['[*:1]~[#7X2+0:2]~[#6X2:3](~[#16X1])', 391.95833137276736, 2.50798739062429], + ['[#1:1]-[#7X2+0:2]~[*:3]', 507.0480341279176, 2.0683870749527142], + ['[#6,#7,#8:1]-[#7X3:2](~[#8X1])~[#8X1:3]', 902.4125500723017, 2.4848064105837495], + ['[#8X1:1]~[#7X3:2]~[#8X1:3]', 1011.7158301063528, 2.574672992442965], + ['[*:1]~[#7X2:2]~[#7X1:3]', 516.8615635448392, 3.141592653589793], + ['[*:1]-[#8:2]-[*:3]', 544.678275491328, 1.9260385591386002], + ['[#6X3,#7:1]~;@[#8;r:2]~;@[#6X3,#7:3]', 662.6580893279216, 1.9628499735294003], + ['[*:1]-[#8X2+1:2]=[*:3]', 298.18885977468403, 2.008811787915724], + ['[*:1]~[#16X4:2]~[*:3]', 740.9812714105369, 1.8017835683224883], + ['[*:1]-[#16X4,#16X3+0:2]-[*:3]', 689.6559543221881, 1.6932105247799079], + ['[*:1]~[#16X3$(*~[#8X1,#7X2]):2]~[*:3]', 938.5243454269897, 1.7842536037311059], + ['[*:1]~[#16X2,#16X3+1:2]~[*:3]', 353.81336531815066, 1.7480520744365022], + ['[*:1]=[#16X2:2]=[*:3]', 585.76, 3.141592653589793], + ['[*:1]=[#16X2:2]=[#8:3]', 588.6329174156912, 2.009110420555779], + ['[#6X3:1]-[#16X2:2]-[#6X3:3]', 583.2403666270457, 1.7363196974046737], + ['[#6X3:1]-[#16X2:2]-[#6X4:3]', 368.6557727886002, 1.6898438115540941], + ['[#6X3:1]-[#16X2:2]-[#1:3]', 517.46334854182, 1.6817374889694794], + ['[*:1]~[#15:2]~[*:3]', 542.8623491348576, 2.7467872840889727]]}, + 'HarmonicBond': { 'patterns': [ ['[#6X4:1]-[#6X4:2]', 221435.25929028585, 0.15219012649500002], + ['[#6X4:1]-[#6X3:2]', 242452.86938808937, 0.1498646816465], + ['[#6X4:1]-[#6X3:2]=[#8X1+0]', 275676.60854900297, 0.15234359583340001], + ['[#6X3:1]-[#6X3:2]', 233580.24861764675, 0.14546289250340003], + ['[#6X3:1]:[#6X3:2]', 301905.092578521, 0.1387193227181], + ['[#6X3:1]=[#6X3:2]', 334016.49833172566, 0.13716885620170002], + ['[#6:1]-[#7:2]', 306553.7072171673, 0.1464762957261], + ['[#6X3:1]-[#7X3:2]', 326381.7456334099, 0.1390160554689], + ['[#6X4:1]-[#7X3:2]-[#6X3]=[#8X1+0]', 294600.92777590535, 0.1450098586273], + ['[#6X3:1](=[#8X1+0])-[#7X3:2]', 412347.817359175, 0.1381897776683], + ['[#6X3:1]-[#7X2:2]', 320113.6702097845, 0.13792443942670002], + ['[#6X3:1]:[#7X2,#7X3+1:2]', 372632.1249267455, 0.13287767121650002], + ['[#6X3:1]=[#7X2,#7X3+1:2]', 422987.30877391365, 0.1304826179986], + ['[#6:1]-[#8:2]', 276118.8797485491, 0.1427343958716], + ['[#6X3:1]-[#8X1-1:2]', 267189.687504641, 0.1267452136112], + ['[#6X4:1]-[#8X2H0:2]', 306888.771877389, 0.1425895053732], + ['[#6X3:1]-[#8X2:2]', 309065.97469870653, 0.13641169861160002], + ['[#6X3:1]-[#8X2H1:2]', 322804.3525587202, 0.1369745182308], + ['[#6X3a:1]-[#8X2H0:2]', 356218.26400112174, 0.1362945806494], + ['[#6X3:1](=[#8X1])-[#8X2H0:2]', 252005.6732566329, 0.1345741232799], + ['[#6:1]=[#8X1+0,#8X2+1:2]', 487602.3277661968, 0.1225198386222], + ['[#6X3:1](~[#8X1])~[#8X1:2]', 483214.20577486564, 0.1256570299838], + ['[#6X3:1]~[#8X2+1:2]~[#6X3]', 455536.89716588, 0.1360087778512], + ['[#6X2:1]-[#6:2]', 681386.398087096, 0.1429562140556], + ['[#6X2:1]-[#6X4:2]', 330928.7969699687, 0.1466849154707], + ['[#6X2:1]=[#6X3:2]', 485919.4836299024, 0.13139165542130002], + ['[#6:1]#[#7:2]', 678728.4582101465, 0.1165766525576], + ['[#6X2:1]#[#6X2:2]', 418663.55583559605, 0.12152834247569999], + ['[#6X2:1]-[#8X2:2]', 292900.7956988829, 0.13215389102060002], + ['[#6X2:1]-[#7:2]', 430627.76919051283, 0.1340739523071], + ['[#6X2:1]=[#7:2]', 504434.62413201603, 0.11961123463920001], + ['[#16:1]=[#6:2]', 247958.00190972944, 0.1669096542113], + ['[#6X2:1]=[#16:2]', 406299.15060936403, 0.1580136796038], + ['[#7:1]-[#7:2]', 353727.930421901, 0.14217865174810002], + ['[#7X3:1]-[#7X2:2]', 347354.16991406726, 0.1355145125452], + ['[#7X2:1]-[#7X2:2]', 282320.9754652233, 0.1350517083318], + ['[#7:1]:[#7:2]', 306171.0155981791, 0.13093368788919998], + ['[#7:1]=[#7:2]', 292018.39181863243, 0.1269478323505], + ['[#7+1:1]=[#7-1:2]', 320329.80017768726, 0.1216477474649], + ['[#7:1]#[#7:2]', 318111.9380605008, 0.11569186730370001], + ['[#7:1]-[#8X2:2]', 251856.32871814267, 0.1411966803189], + ['[#7:1]~[#8X1:2]', 311627.28561451397, 0.12326792361050001], + ['[#8X2:1]-[#8X2:2]', 251066.25806949835, 0.1451203938761], + ['[#16:1]-[#6:2]', 198321.6, 0.18100000000000002], + ['[#16:1]-[#1:2]', 251429.26169251613, 0.1349811192379], + ['[#16:1]-[#16:2]', 133615.61670769064, 0.2088681010671], + ['[#16:1]-[#9:2]', 313800.0, 0.16000000000000003], + ['[#16:1]-[#17:2]', 192642.49388198546, 0.20906897400820001], + ['[#16:1]-[#35:2]', 141035.44443307433, 0.22360893159109999], + ['[#16:1]-[#53:2]', 62760.00000000001, 0.26], + ['[#16X2,#16X1-1,#16X3+1:1]-[#6X4:2]', 203624.45285584097, 0.18341683120850003], + ['[#16X2,#16X1-1,#16X3+1:1]-[#6X3:2]', 225745.02380023338, 0.17601808996420001], + ['[#16X2:1]-[#7:2]', 251470.58738536187, 0.1685967252936], + ['[#16X2:1]-[#8X2:2]', 215233.6305821103, 0.16707412736420002], + ['[#16X2:1]=[#8X1,#7X2:2]', 253527.30879169187, 0.1466630613191], + ['[#16X4,#16X3!+1:1]-[#6:2]', 244340.78717618005, 0.1820747756121], + ['[#16X4,#16X3:1]~[#7:2]', 189398.23036241296, 0.1756360403467], + ['[#16X4,#16X3:1]-[#8X2:2]', 256157.23428347148, 0.1679763690372], + ['[#16X4,#16X3:1]~[#8X1:2]', 465199.37537255284, 0.14762696155090002], + ['[#15:1]-[#1:2]', 167414.3768569325, 0.1409632139658], + ['[#15:1]~[#6:2]', 134879.3950826415, 0.1834505699142], + ['[#15:1]-[#7:2]', 262091.43559897068, 0.175691942977], + ['[#15:1]=[#7:2]', 343273.0341485299, 0.15930215541610002], + ['[#15:1]~[#8X2:2]', 227238.41570344867, 0.1644080332096], + ['[#15:1]~[#8X1:2]', 462981.098502852, 0.14909910610090002], + ['[#16:1]-[#15:2]', 124689.57653277545, 0.21363191236870002], + ['[#15:1]=[#16X1:2]', 192464.00000000003, 0.198], + ['[#6:1]-[#9:2]', 338389.78763913346, 0.1352926211207], + ['[#6X4:1]-[#9:2]', 284889.5726108851, 0.1358690858053], + ['[#6:1]-[#17:2]', 182441.45424436082, 0.17460660370620001], + ['[#6X4:1]-[#17:2]', 144269.52641897602, 0.1791475381368], + ['[#6:1]-[#35:2]', 170701.16614289136, 0.1911636491655], + ['[#6X4:1]-[#35:2]', 153516.0080841569, 0.1982556193834], + ['[#6:1]-[#53:2]', 150029.1257168652, 0.2245041183821], + ['[#6X4:1]-[#53:2]', 123846.40000000001, 0.21660000000000001], + ['[#7:1]-[#9:2]', 132999.34702086123, 0.1416240764058], + ['[#7:1]-[#17:2]', 125503.85336275873, 0.18029715656030001], + ['[#7:1]-[#35:2]', 82786.56635643673, 0.19646281561720003], + ['[#7:1]-[#53:2]', 66944.0, 0.21000000000000002], + ['[#15:1]-[#9:2]', 368192.00000000006, 0.164], + ['[#15:1]-[#17:2]', 137970.8581606005, 0.20486700457649998], + ['[#15:1]-[#35:2]', 98128.02171338256, 0.2239759015052], + ['[#15:1]-[#53:2]', 58576.00000000001, 0.26], + ['[#6X4:1]-[#1:2]', 309655.084322414, 0.10938994926340001], + ['[#6X3:1]-[#1:2]', 332422.63167531794, 0.10853584959160001], + ['[#6X2:1]-[#1:2]', 380602.5962746914, 0.1071345693716], + ['[#7:1]-[#1:2]', 422704.91441430245, 0.10194818650269999], + ['[#8:1]-[#1:2]', 454823.2121721368, 0.09716763312559001]]}, + 'ImproperTorsion': { 'patterns': [ ['[*:1]~[#6X3:2](~[*:3])~[*:4]', 1.5341333333333333, 3.141592653589793, 2.0], + ['[*:1]~[#6X3:2](~[#8X1:3])~[#8:4]', 14.644, 3.141592653589793, 2.0], + ['[*:1]~[#7X3$(*~[#15,#16](!-[*])):2](~[*:3])~[*:4]', 1.5341333333333333, 3.141592653589793, 2.0], + ['[*:1]~[#7X3$(*~[#6X3]):2](~[*:3])~[*:4]', 1.3946666666666667, 3.141592653589793, 2.0], + ['[*:1]~[#7X3$(*~[#7X2]):2](~[*:3])~[*:4]', 1.5341333333333333, 3.141592653589793, 2.0], + ['[*:1]~[#7X3$(*@1-[*]=,:[*][*]=,:[*]@1):2](~[*:3])~[*:4]', 14.644, 3.141592653589793, 2.0], + ['[*:1]~[#6X3:2](=[#7X2,#7X3+1:3])~[#7:4]', 14.644, 3.141592653589793, 2.0]]}, + 'LennardJones': { 'patterns': [ ['[#1:1]', 0.10690784617684071, 0.256298263747533], + ['[#1:1]-[#6X4]', 0.2644543413268125, 0.25694621241774834], + ['[#1:1]-[#6X4]-[#7,#8,#9,#16,#17,#35]', 0.2583225710839196, 0.2620234443329566], + ['[#1:1]-[#6X4](-[#7,#8,#9,#16,#17,#35])-[#7,#8,#9,#16,#17,#35]', 0.22931733004932334, 0.256298263747533], + ['[#1:1]-[#6X4](-[#7,#8,#9,#16,#17,#35])(-[#7,#8,#9,#16,#17,#35])-[#7,#8,#9,#16,#17,#35]', 0.21149935568651657, 0.256298263747533], + ['[#1:1]-[#6X4]~[*+1,*+2]', 0.19599771799087468, 0.256298263747533], + ['[#1:1]-[#6X3]', 0.25725815350632797, 0.25557359011362957], + ['[#1:1]-[#6X3]~[#7,#8,#9,#16,#17,#35]', 0.2453626527729973, 0.2341787379182071], + ['[#1:1]-[#6X3](~[#7,#8,#9,#16,#17,#35])~[#7,#8,#9,#16,#17,#35]', 0.24419227541121594, 0.24882224592422147], + ['[#1:1]-[#6X2]', 0.259964245953351, 0.25051946032194783], + ['[#1:1]-[#7]', 0.11034276772973169, 0.24280850252853783], + ['[#1:1]-[#8]', 0.053453923088366904, 0.007181363561702421], + ['[#1:1]-[#16]', 0.10690784617684071, 0.256298263747533], + ['[#6:1]', 0.3480646886945065, 0.6029121460360367], + ['[#6X2:1]', 0.33996695084235345, 0.9373579892442374], + ['[#6X4:1]', 0.3379531761626621, 0.6748252485722644], + ['[#8:1]', 0.30398122050658094, 0.937817853158963], + ['[#8X2H0+0:1]', 0.3025106490435313, 0.8395583046153953], + ['[#8X2H1+0:1]', 0.29971599872486376, 0.9361822790544445], + ['[#7:1]', 0.32068760236639016, 0.8376283775860281], + ['[#16:1]', 0.35635948725613575, 1.0227414140436477], + ['[#15:1]', 0.37417746161894255, 0.9147677300823418], + ['[#9:1]', 0.3118145513491188, 0.5051969912816188], + ['[#17:1]', 0.3307527806460625, 1.0541683157195045], + ['[#35:1]', 0.3509796339398518, 1.1605274212716914], + ['[#53:1]', 0.41872239752595947, 1.2936769303036983], + ['[#3+1:1]', 0.18263423721876954, 0.3422111722314162], + ['[#11+1:1]', 0.24392806902682487, 0.6048520738164002], + ['[#19+1:1]', 0.3037964628858557, 0.9002051175148917], + ['[#37+1:1]', 0.32303987519768707, 1.1711561935113524], + ['[#55+1:1]', 0.3520831734090621, 1.3042088979914221], + ['[#9X0-1:1]', 0.4103479495754403, 0.11863800402906313], + ['[#17X0-1:1]', 0.44776569573733455, 0.3858921403708554], + ['[#35X0-1:1]', 0.46469277138200105, 0.495392968864113], + ['[#53X0-1:1]', 0.5095940667762741, 0.47392384873521615], + ['[#1]-[#8X2H2+0:1]-[#1]', 0.31507, 0.7977383029540452], + ['[#1:1]-[#8X2H2+0]-[#1]', 0.1, 0.0]], + 'props': {'combining_rules': 'Lorentz-Berthelot', 'method': 'cutoff', 'potential': 'Lennard-Jones-12-6', 'scale12': 0.0, 'scale13': 0.0, 'scale14': 0.5, 'scale15': 1.0}}, + 'ProperTorsion': { 'patterns': [ ['[*:1]-[#6X4:2]-[#6X4:3]-[*:4]', [[0.4834078610240992, 0.0, 3.0]]], + ['[#6X4:1]-[#6X4:2]-[#6X4:3]-[#6X4:4]', [[0.3859268900186583, 0.0, 3.0], [1.3221171897434616, 3.141592653589793, 2.0], [1.358878183988969, 3.141592653589793, 1.0]]], + ['[#1:1]-[#6X4:2]-[#6X4:3]-[#1:4]', [[0.7999501384731329, 0.0, 3.0]]], + ['[#1:1]-[#6X4:2]-[#6X4:3]-[#6X4:4]', [[0.43541375381115444, 0.0, 3.0]]], + ['[#8X2:1]-[#6X4:2]-[#6X4:3]-[#8X2:4]', [[-0.1295661609826928, 0.0, 3.0], [1.923883314372188, 0.0, 2.0]]], + ['[#9:1]-[#6X4:2]-[#6X4:3]-[#9:4]', [[0.09056732791648081, 0.0, 3.0], [-0.7339361307159632, 3.141592653589793, 1.0]]], + ['[#17:1]-[#6X4:2]-[#6X4:3]-[#17:4]', [[1.2606288350851655, 0.0, 3.0], [-2.2309070239317483, 3.141592653589793, 1.0]]], + ['[#35:1]-[#6X4:2]-[#6X4:3]-[#35:4]', [[0.3600729178607652, 0.0, 3.0], [-0.6537533800155304, 3.141592653589793, 1.0]]], + ['[#1:1]-[#6X4:2]-[#6X4:3]-[#8X2:4]', [[0.4667359389899568, 0.0, 3.0], [1.436153909161781, 0.0, 1.0]]], + ['[#1:1]-[#6X4:2]-[#6X4:3]-[#9:4]', [[0.4189792342352832, 0.0, 3.0], [1.4026079432474656, 0.0, 1.0]]], + ['[#1:1]-[#6X4:2]-[#6X4:3]-[#17:4]', [[0.665506814003332, 0.0, 3.0], [1.2352749283906015, 0.0, 1.0]]], + ['[#1:1]-[#6X4:2]-[#6X4:3]-[#35:4]', [[0.9231491272611657, 0.0, 3.0], [3.256364344883778, 0.0, 1.0]]], + ['[*:1]-[#6X4:2]-[#6X4;r3:3]-[*:4]', [[0.3061255751227515, 0.0, 1.0]]], + ['[*:1]-[#6X4:2]-[#6X4;r3:3]-[#6X4;r3:4]', [[2.0362010854964656, 0.0, 3.0]]], + ['[*:1]-[#6X4;r3:2]-@[#6X4;r3:3]-[*:4]', [[-5.247457475575384, 0.0, 2.0]]], + ['[#6X4;r3:1]-[#6X4;r3:2]-[#6X4;r3:3]-[*:4]', [[15.298209578308768, 0.0, 2.0], [14.045841771639935, 0.0, 1.0]]], + ['[*:1]~[#6X3:2]-[#6X4:3]-[*:4]', [[0.11150103813943592, 0.0, 3.0]]], + ['[*:1]-[#6X4:2]-[#6X3:3]=[*:4]', [[-1.5494826496908296, 0.0, 2.0]]], + ['[#1:1]-[#6X4:2]-[#6X3:3]=[#8X1:4]', [[1.9002374703341824, 0.0, 1.0], [0.6232948633450065, 0.0, 2.0], [0.12384754142306545, 3.141592653589793, 3.0]]], + ['[#1:1]-[#6X4:2]-[#6X3:3]=[#6X3:4]', [[1.2394205363392297, 3.141592653589793, 3.0], [0.031108134750566938, 0.0, 1.0]]], + ['[#6X3:1]-[#6X4:2]-[#6X3:3]=[#6X3:4]', [[-0.15366696447504963, 0.0, 3.0], [0.7168100684287289, 3.141592653589793, 2.0]]], + ['[#7X3:1]-[#6X4:2]-[#6X3:3]-[#7X3:4]', [[-1.5563396681799424, 3.141592653589793, 1.0], [1.6828598813972617, 3.141592653589793, 2.0]]], + ['[#6X4:1]-[#6X4:2]-[#6X3:3]-[#7X3:4]', [[-0.19086666442571865, 0.0, 4.0], [0.6427004856486841, 0.0, 2.0]]], + ['[#16X2,#16X1-1,#16X3+1:1]-[#6X3:2]-[#6X4:3]-[#1:4]', [[-0.5049763761237984, 0.0, 2.0], [-1.867745459418064, 3.141592653589793, 1.0]]], + ['[#16X2,#16X1-1,#16X3+1:1]-[#6X3:2]-[#6X4:3]-[#7X4,#7X3:4]', [[0.428678730800356, 0.0, 4.0], [0.4291204734832144, 0.0, 3.0], [-3.0421199951100735, 0.0, 2.0], [1.1381690831295002, 4.71238898038469, 2.0], [1.8891551708638703, 1.5707963267948966, 1.0]]], + ['[#16X2,#16X1-1,#16X3+1:1]-[#6X3:2]-[#6X4:3]-[#7X3$(*-[#6X3,#6X2]):4]', [[1.5118268282911456, 4.71238898038469, 4.0], [2.968174659128597, 0.0, 3.0], [1.2410369183714896, 3.141592653589793, 2.0], [1.6004936783356714, 4.71238898038469, 2.0], [0.041897166083462244, 4.71238898038469, 1.0], [-0.21178211477307193, 0.0, 1.0]]], + ['[*:1]-[#6X4;r3:2]-[#6X3:3]~[*:4]', [[0.5582971568453936, 0.0, 1.0]]], + ['[#6X4:1]-[#6X4;r3:2]-[#6X3:3]~[#6X3:4]', [[0.5641813600131783, 3.141592653589793, 4.0], [3.3368978410874552, 3.141592653589793, 2.0]]], + ['[#1:1]-[#6X4;r3:2]-[#6X3:3]~[#6X3:4]', [[-0.1503253114403293, 3.141592653589793, 4.0], [-0.6435181740964936, 0.0, 3.0], [1.678418272887964, 3.141592653589793, 2.0]]], + ['[#6X3:1]-[#6X4;r3:2]-[#6X3:3]-[#7X3:4]', [[6.258784664952016, 0.0, 2.0], [3.3401843274806, 3.141592653589793, 1.0], [0.4331152279896504, 3.141592653589793, 3.0]]], + ['[#6X3:1]-[#6X4;r3:2]-[#6X3:3]=[#8X1:4]', [[11.156683268053825, 3.141592653589793, 2.0], [-2.3112916720714636, 3.141592653589793, 1.0], [1.3482711737859177, 3.141592653589793, 3.0]]], + ['[#6X3:1]-[#6X4;r3:2]-[#6X3:3]~[#6X3:4]', [[2.307109338828531, 3.141592653589793, 2.0]]], + ['[#7X3:1]-[#6X4;r3:2]-[#6X3:3]~[#6X3:4]', [[0.8193618281981344, 3.141592653589793, 4.0], [0.6441402107923592, 3.141592653589793, 2.0]]], + ['[#6X4;r3:1]-;@[#6X4;r3:2]-[#6X3:3]~[#6X3:4]', [[0.22493750535582735, 3.141592653589793, 4.0], [-4.358837934157072, 0.0, 3.0], [5.773875388547689, 3.141592653589793, 2.0]]], + ['[#6X4;r3:1]-;@[#6X4;r3:2]-[#6X3;r6:3]:[#6X3;r6:4]', [[0.282908840481982, 3.141592653589793, 4.0], [2.6090067804520185, 3.141592653589793, 2.0]]], + ['[#6X4;r3:1]-;@[#6X4;r3:2]-[#6X3;r5:3]-;@[#6X3;r5:4]', [[-0.3990201061349338, 3.141592653589793, 4.0], [1.1718936136456097, 0.0, 3.0], [1.9534657751723234, 3.141592653589793, 2.0]]], + ['[#6X4;r3:1]-;@[#6X4;r3:2]-[#6X3;r5:3]=;@[#6X3;r5:4]', [[1.201487517528008, 3.141592653589793, 1.0]]], + ['[#6X4;r3:1]-;@[#6X4;r3:2]-[#6X3:3]-[#6X4:4]', [[3.1929485821530053, 0.0, 1.0]]], + ['[#6X4;r3:1]-;@[#6X4;r3:2]-[#6X3;r6:3]:[#7X2;r6:4]', [[10.7477938739528, 3.141592653589793, 2.0], [-1.2033011681344954, 3.141592653589793, 1.0], [4.798450485600104, 0.0, 3.0]]], + ['[#6X4;r3:1]-;@[#6X4;r3:2]-[#6X3:3]=[#7X2:4]', [[-3.0115791261520353, 0.0, 3.0], [7.002047885001584, 3.141592653589793, 2.0], [-0.19052905476398432, 3.141592653589793, 1.0]]], + ['[#6X4;r3:1]-;@[#6X4;r3:2]-[#6X3:3]-[#8X2:4]', [[-1.517747737728168, 3.141592653589793, 4.0], [6.5829418777034885, 3.141592653589793, 2.0]]], + ['[#6X4;r3:1]-;@[#6X4;r3:2]-[#6X3:3]=[#8X1:4]', [[-3.9592503385778186, 5.585053606381854, 2.0]]], + ['[*:1]~[#6X3:2]-[#6X3:3]~[*:4]', [[4.866977563956776, 3.141592653589793, 2.0]]], + ['[*:1]~[#6X3:2]:[#6X3:3]~[*:4]', [[15.321515534533985, 3.141592653589793, 2.0]]], + ['[*:1]-,:[#6X3:2]=[#6X3:3]-,:[*:4]', [[22.15362310384957, 3.141592653589793, 2.0]]], + ['[#6X4:1]-[#6X3:2]=[#6X3:3]-[#6X4:4]', [[21.26408805391445, 3.141592653589793, 2.0], [6.923245769276832, 3.141592653589793, 1.0]]], + ['[*:1]~[#6X3:2]-[#6X3$(*=[#8,#16,#7]):3]~[*:4]', [[4.173190890070053, 3.141592653589793, 2.0]]], + ['[#6X3:1]=[#6X3:2]-[#6X3:3]=[#8X1:4]', [[0.9822507508279056, 3.141592653589793, 2.0], [1.9863410844530769, 0.0, 3.0]]], + ['[*:1]~[#7a:2]:[#6a:3]~[*:4]', [[19.098854555757242, 3.141592653589793, 2.0]]], + ['[*:1]-[#6X4:2]-[#7X4:3]-[*:4]', [[0.2802181945489924, 0.0, 3.0]]], + ['[*:1]-[#6X4:2]-[#7X3:3]-[*:4]', [[0.8521427965991905, 0.0, 3.0]]], + ['[*:1]-[#6X4:2]-[#7X3:3]-[#7X2:4]=[#6]', [[2.071270223224491, 0.0, 3.0], [0.057203350824998486, 3.141592653589793, 2.0]]], + ['[#1:1]-[#6X4:2]-[#7X3:3]-[#7X2:4]=[#6]', [[2.2085101016163913, 0.0, 3.0], [-0.6955013865397448, 3.141592653589793, 2.0]]], + ['[*:1]-[#6X4:2]-[#7X3:3]-[#7X2:4]=[#7X2,#8X1]', [[0.8846195543813928, 0.0, 3.0], [-0.18028825964699946, 3.141592653589793, 2.0]]], + ['[#1:1]-[#6X4:2]-[#7X3:3]-[#7X2:4]=[#7X2,#8X1]', [[-0.1495851798680556, 0.0, 3.0], [-0.048253241518049196, 3.141592653589793, 2.0]]], + ['[*:1]-[#6X4:2]-[#7X3$(*@1-[*]=,:[*][*]=,:[*]@1):3]-[*:4]', [[6.799405604633456, 3.141592653589793, 2.0]]], + ['[#1:1]-[#6X4:2]-[#7X3$(*@1-[*]=,:[*][*]=,:[*]@1):3]-[*:4]', [[8.100999034080745, 3.141592653589793, 2.0]]], + ['[#6X4:1]-[#6X4:2]-[#7X4,#7X3:3]-[#6X4:4]', [[1.1760757362760232, 0.0, 3.0], [0.22277947274788648, 3.141592653589793, 2.0]]], + ['[#1:1]-[#7X4,#7X3:2]-[#6X4;r3:3]-[*:4]', [[3.7577628821919946, 0.0, 1.0]]], + ['[#1:1]-[#7X4,#7X3:2]-[#6X4;r3:3]-[#6X4;r3:4]', [[4.351521443849104, 0.0, 1.0]]], + ['[!#1:1]-[#7X4,#7X3:2]-[#6X4;r3:3]-[*:4]', [[1.8191764261225047, 0.0, 3.0]]], + ['[!#1:1]-[#7X4,#7X3:2]-[#6X4;r3:3]-[#6X4;r3:4]', [[4.9187588766440635, 0.0, 3.0]]], + ['[*:1]-[#7X4:2]-[#6X3:3]~[*:4]', [[-0.0008535872512958809, 0.0, 1.0]]], + ['[*:1]-[#6X4:2]-[#7X3$(*~[#6X3,#6X2]):3]~[*:4]', [[0.7624858976858641, 0.0, 2.0], [0.12436280495163224, 0.0, 3.0]]], + ['[*:1]-[#6X4:2]-[#7X3$(*~[#8X1]):3]~[#8X1:4]', [[0.008561195737914855, 0.0, 3.0]]], + ['[#6X3:1]-[#7X3:2]-[#6X4:3]-[#6X3:4]', [[-2.88349933908296, 3.141592653589793, 2.0], [1.1413309473023527, 0.0, 1.0]]], + ['[#6X4:1]-[#6X4:2]-[#7X3:3]-[#6X3:4]=[#8,#16,#7]', [[-0.6228629448397511, 3.141592653589793, 4.0], [-0.5147696321645312, 3.141592653589793, 3.0], [1.2298012233513784, 0.0, 2.0], [-1.2652947463667161, 0.0, 1.0]]], + ['[#8X2H0:1]-[#6X4:2]-[#7X3:3]-[#6X3:4]', [[2.194849481896706, 0.0, 2.0], [8.205371428041328, 0.0, 1.0]]], + ['[#6X3:1]-[#7X3:2]-[#6X4;r3:3]-[#6X4;r3:4]', [[3.7877545873566403, 0.0, 3.0], [-4.039180259921616, 0.0, 2.0], [-3.37289253134562, 0.0, 1.0]]], + ['[*:1]~[#7X2:2]-[#6X4:3]-[*:4]', [[-5.786359197460464, 0.0, 1.0]]], + ['[#6X3:1]=[#7X2,#7X3+1:2]-[#6X4:3]-[#1:4]', [[3.1645188000862423, 0.0, 1.0]]], + ['[#6X3:1]=[#7X2,#7X3+1:2]-[#6X4:3]-[#6X3,#6X4:4]', [[3.764171213554459, 0.0, 1.0]]], + ['[*:1]~[#7X3,#7X2-1:2]-[#6X3:3]~[*:4]', [[2.9350078297984674, 3.141592653589793, 2.0]]], + ['[*:1]~[#7X3,#7X2-1:2]-!@[#6X3:3]~[*:4]', [[4.991902713141489, 3.141592653589793, 2.0]]], + ['[*:1]-[#7X3:2]-[#6X3$(*=[#8,#16,#7]):3]~[*:4]', [[7.569054279772552, 3.141592653589793, 2.0], [-0.20151306284204928, 0.0, 1.0]]], + ['[#1:1]-[#7X3:2]-[#6X3:3]=[#8,#16,#7:4]', [[2.0338395226397696, 3.141592653589793, 2.0], [3.7308670004513265, 0.0, 1.0]]], + ['[*:1]-[#7X3:2]-!@[#6X3:3](=[#8,#16,#7:4])-[#6,#1]', [[12.978553656010488, 3.141592653589793, 2.0], [-1.7791009333444447, 0.0, 1.0]]], + ['[#1:1]-[#7X3:2]-!@[#6X3:3](=[#8,#16,#7:4])-[#6,#1]', [[9.825603686165657, 3.141592653589793, 2.0], [5.255757435827624, 0.0, 1.0]]], + ['[*:1]-[#7X3:2]-!@[#6X3:3](=[#8,#16,#7:4])-[#7X3]', [[4.511131555340432, 3.141592653589793, 2.0], [0.0, 0.0, 1.0]]], + ['[*:1]-[#7X3;r5:2]-@[#6X3;r5:3]~[*:4]', [[6.596778357661841, 3.141592653589793, 2.0]]], + ['[#8X1:1]~[#7X3:2]~[#6X3:3]~[*:4]', [[3.332153080893722, 3.141592653589793, 2.0]]], + ['[*:1]=[#7X2,#7X3+1:2]-[#6X3:3]-[*:4]', [[7.375631434630576, 3.141592653589793, 2.0]]], + ['[*:1]=[#7X2,#7X3+1:2]-[#6X3:3]=,:[*:4]', [[6.487356510016576, 3.141592653589793, 2.0]]], + ['[*:1]~[#7X2,#7X3$(*~[#8X1]):2]:[#6X3:3]~[*:4]', [[15.614143831453664, 3.141592653589793, 2.0]]], + ['[#6X3:1]:[#7X2:2]:[#6X3:3]:[#6X3:4]', [[24.741172824496353, 3.141592653589793, 2.0]]], + ['[*:1]-,:[#6X3:2]=[#7X2:3]-[*:4]', [[28.186614206504334, 3.141592653589793, 2.0]]], + ['[*:1]-[#7X3+1:2]=,:[#6X3:3]-,:[*:4]', [[13.963305839817624, 3.141592653589793, 2.0]]], + ['[#16X4,#16X3+0:1]-[#7X2:2]=[#6X3:3]-[#7X3:4]', [[27.783661002412504, 3.141592653589793, 2.0]]], + ['[#16X4,#16X3+0:1]-[#7X2:2]=[#6X3:3]-[#16X2,#16X3+1:4]', [[26.799825098752194, 3.141592653589793, 2.0]]], + ['[#7X2:1]~[#7X2:2]-[#6X3:3]~[#6X3:4]', [[8.231003223821624, 3.141592653589793, 2.0]]], + ['[#7X2:1]~[#7X2:2]-[#6X4:3]-[#6X3:4]', [[0.612895448041148, 0.0, 2.0]]], + ['[#7X2:1]~[#7X2:2]-[#6X4:3]~[#1:4]', [[0.18901223970703865, 0.0, 2.0]]], + ['[*:1]-[#6X4:2]-[#8X2:3]-[#1:4]', [[1.2662416460753056, 0.0, 3.0]]], + ['[#6X4:1]-[#6X4:2]-[#8X2H1:3]-[#1:4]', [[1.4415478912558233, 0.0, 3.0], [0.5709303865899752, 0.0, 1.0]]], + ['[*:1]-[#6X4:2]-[#8X2H0:3]-[*:4]', [[0.1982205069773368, 0.0, 3.0]]], + ['[#6X4:1]-[#6X4:2]-[#8X2H0:3]-[#6X4:4]', [[1.9046222643426256, 0.0, 3.0], [-0.15429518323388106, 3.141592653589793, 2.0]]], + ['[#6X4:1]-[#6X4:2]-[#8X2:3]-[#6X3:4]', [[0.8026051617598312, 0.0, 3.0], [3.8501920602895257, 3.141592653589793, 1.0]]], + ['[#6X4:1]-[#8X2:2]-[#6X4:3]-[#8X2:4]', [[1.6261093923613528, 0.0, 3.0], [1.0743136963378241, 3.141592653589793, 2.0], [5.521296064998616, 3.141592653589793, 1.0]]], + ['[#6X4:1]-[#8X2:2]-[#6X4:3]-[#7X3:4]', [[2.099802860026865, 0.0, 3.0], [2.6288540943573535, 0.0, 2.0]]], + ['[#6X3:1]-[#8X2:2]-[#6X4;r3:3]-@[#6X4;r3:4]', [[-1.6239958458157209, 0.0, 1.0]]], + ['[#6X3:1]-[#8X2:2]-[#6X4;r3:3]-[#1:4]', [[1.0444864343628488, 0.0, 1.0]]], + ['[#1:1]-[#8X2:2]-[#6X4;r3:3]-[#1:4]', [[1.9195827618477586, 0.0, 3.0], [1.4231918929551255, 0.0, 2.0], [2.9787111337124097, 0.0, 1.0]]], + ['[#1:1]-[#8X2:2]-[#6X4;r3:3]-[#6X4:4]', [[1.4049653405668985, 0.0, 3.0], [0.4794822183329984, 0.0, 2.0], [2.7458953965944985, 0.0, 1.0]]], + ['[#1:1]-[#8X2:2]-[#6X4;r3:3]-[#6X4;r3:4]', [[1.8319866307375794, 0.0, 3.0], [0.5449737736238888, 0.0, 2.0], [2.9207126757347504, 0.0, 1.0]]], + ['[*:1]~[#6X3:2]-[#8X2:3]-[*:4]', [[6.410861677363401, 3.141592653589793, 2.0]]], + ['[*:1]~[#6X3:2]-[#8X2:3]-[#1:4]', [[3.465619998873573, 3.141592653589793, 2.0]]], + ['[*:1]~[#6X3:2](=[#8,#16,#7])-[#8X2H0:3]-[*:4]', [[11.63090805571212, 3.141592653589793, 2.0]]], + ['[*:1]~[#6X3:2](=[#8,#16,#7])-[#8:3]-[#1:4]', [[11.244323820487825, 3.141592653589793, 2.0]]], + ['[#1:1]-[#8X2:2]-[#6X3:3]=[#8X1:4]', [[11.600591494029992, 3.141592653589793, 2.0], [0.4055419386056916, 0.0, 1.0]]], + ['[#8,#16,#7:1]=[#6X3:2]-[#8X2H0:3]-[#6X4:4]', [[4.869377577647952, 3.141592653589793, 2.0], [0.7025718355151895, 3.141592653589793, 1.0]]], + ['[*:1]-[#8X2:2]@[#6X3:3]~[*:4]', [[6.944248375511808, 3.141592653589793, 2.0]]], + ['[*:1]-[#8X2+1:2]=[#6X3:3]-[*:4]', [[16.550833033804224, 3.141592653589793, 2.0]]], + ['[*:1]=[#8X2+1:2]-[#6:3]~[*:4]', [[0.9338762218314952, 3.141592653589793, 2.0]]], + ['[*:1]~[#16:2]=,:[#6:3]~[*:4]', [[19.04257218967942, 3.141592653589793, 2.0]]], + ['[*:1]-[#16X2,#16X3+1:2]-[#6:3]~[*:4]', [[2.1137682084044345, 3.141592653589793, 2.0]]], + ['[*:1]-[#16X2,#16X3+1:2]-[#6:3]-[#1:4]', [[1.1250399664760298, 0.0, 3.0]]], + ['[#6X3:1]-@[#16X2,#16X1-1,#16X3+1:2]-@[#6X3,#7X2;r5:3]=@[#6,#7;r5:4]', [[15.584261074401818, 3.141592653589793, 2.0]]], + ['[*:1]~[#16X4,#16X3!+1:2]-[#6X4:3]-[*:4]', [[0.46364243381368725, 0.0, 3.0]]], + ['[#6X4:1]-[#16X4,#16X3+0:2]-[#6X4:3]-[#1:4]', [[-0.3430479349229657, 0.0, 1.0]]], + ['[#6X4:1]-[#16X4,#16X3+0:2]-[#6X4:3]~[#6X4:4]', [[1.110719161027124, 0.0, 3.0]]], + ['[*:1]~[#16X4,#16X3+0:2]-[#6X3:3]~[*:4]', [[2.0015490563408576, 3.141592653589793, 2.0]]], + ['[#6:1]-[#16X4,#16X3+0:2]-[#6X3:3]~[*:4]', [[2.7963135304791975, 0.0, 1.0]]], + ['[*:1]~[#15:2]-[#6:3]-[*:4]', [[0.006150692719400889, 0.0, 1.0]]], + ['[*:1]~[#15:2]-[#6X3:3]~[*:4]', [[4.189515782530177, 0.0, 1.0]]], + ['[*:1]-[#8:2]-[#8:3]-[*:4]', [[1.916159606781184, 0.0, 1.0]]], + ['[*:1]-[#8:2]-[#8H1:3]-[*:4]', [[1.6776422087315241, 0.0, 2.0]]], + ['[*:1]~[#8X2:2]-[#7:3]~[*:4]', [[4.251814365453824, 0.0, 1.0]]], + ['[*:1]-[#8X2r5:2]-;@[#7X3r5:3]~[*:4]', [[3.7214019145426898, 0.0, 1.0]]], + ['[*:1]-[#8X2r5:2]-;@[#7X2r5:3]~[*:4]', [[-1.701731550531604, 0.0, 1.0]]], + ['[*:1]-[#7X4,#7X3:2]-[#7X4,#7X3:3]-[*:4]', [[3.2480834496413307, 0.0, 3.0]]], + ['[#1:1]-[#7X4,#7X3:2]-[#7X4,#7X3:3]-[#1:4]', [[1.6390166232360257, 0.0, 3.0], [3.9613203960684684, 0.0, 2.0], [1.140808246232417, 0.0, 1.0]]], + ['[#6X4:1]-[#7X4,#7X3:2]-[#7X4,#7X3:3]-[#1:4]', [[3.239048838539544, 0.0, 3.0], [4.550140068732889, 0.0, 2.0], [6.354972746842896, 0.0, 1.0]]], + ['[#6X4:1]-[#7X4,#7X3:2]-[#7X4,#7X3:3]-[#6X4:4]', [[2.828997262856652, 0.0, 3.0], [6.916200489007784, 0.0, 2.0], [7.167965250031512, 0.0, 1.0]]], + ['[*:1]-[#7X4,#7X3:2]-[#7X3$(*~[#6X3,#6X2]):3]~[*:4]', [[0.08686961916027361, 0.0, 1.0]]], + ['[*:1]-[#7X3$(*-[#6X3,#6X2]):2]-[#7X3$(*-[#6X3,#6X2]):3]-[*:4]', [[0.08456234290744608, 0.0, 1.0]]], + ['[*:1]-[#7X3$(*-[#6X3,#6X2])r5:2]-@[#7X3$(*-[#6X3,#6X2])r5:3]~[*:4]', [[1.3243464271869225, 3.141592653589793, 2.0]]], + ['[*:1]@[#7X2:2]@[#7X2:3]@[#7X2,#6X3:4]', [[0.9725632274503648, 3.141592653589793, 1.0]]], + ['[*:1]~[#7X2:2]-[#7X3:3]~[*:4]', [[-0.39793402370647496, 0.0, 3.0], [6.503758359007104, 3.141592653589793, 2.0]]], + ['[*:1]=[#7X2:2]-[#7X2:3]=[*:4]', [[9.441394734012697, 3.141592653589793, 2.0]]], + ['[*:1]~[#7X2:2]=,:[#7X2:3]~[*:4]', [[30.477779012087, 3.141592653589793, 2.0]]], + ['[*:1]~[#7X3+1:2]=,:[#7X2:3]~[*:4]', [[13.567479207010336, 3.141592653589793, 2.0]]], + ['[*:1]-[#16X2,#16X3+1:2]-[!#6:3]~[*:4]', [[0.5367146709630096, 3.141592653589793, 2.0]]], + ['[*:1]~[#16X4,#16X3+0:2]-[#7:3]~[*:4]', [[9.329080767876176, 0.0, 1.0]]], + ['[#6X4:1]-[#16X4,#16X3+0:2]-[#7X4,#7X3:3]-[#1:4]', [[1.9600977023528785, 0.0, 1.0]]], + ['[#6X3:1]-[#16X4,#16X3+0:2]-[#7X4,#7X3:3]-[#1:4]', [[0.6920642554956313, 0.0, 3.0]]], + ['[#6X4:1]-[#16X4,#16X3+0:2]-[#7X4,#7X3:3]-[#6X4:4]', [[5.249194015905784, 0.0, 1.0], [-0.24688050599598427, 0.0, 3.0]]], + ['[#6X3:1]-[#16X4,#16X3+0:2]-[#7X4,#7X3:3]-[#6X4:4]', [[-0.8322930098896056, 0.0, 3.0], [-1.1379700439249656, 0.0, 2.0], [4.375362314482929, 0.0, 1.0]]], + ['[#8X1:1]~[#16X4,#16X3+0:2]-[#7X4,#7X3:3]-[#1:4]', [[-6.3811142972005035, 3.141592653589793, 1.0], [0.655748859404884, 0.0, 3.0]]], + ['[#8X1:1]~[#16X4,#16X3+0:2]-[#7X4,#7X3:3]-[#6X4:4]', [[1.8942097147104842, 0.0, 3.0], [4.532715447996583, 3.141592653589793, 2.0], [10.207095443696033, 0.0, 1.0]]], + ['[#6X3:1]-[#16X4,#16X3+0:2]-[#7X3:3]-[#6X3:4]', [[2.793459166256295, 0.0, 3.0], [1.9057876164743401, 0.0, 2.0], [0.6855457262160553, 0.0, 1.0]]], + ['[#6X4:1]-[#16X4,#16X3+0:2]-[#7X3:3]-[#6X3:4]', [[-2.666736667194789, 1.5707963267948966, 3.0], [1.244300434736849, 0.0, 2.0]]], + ['[#8X1:1]~[#16X4,#16X3+0:2]-[#7X3:3]-[#6X3:4]', [[2.107826632475972, 0.0, 1.0]]], + ['[#8X1:1]~[#16X4,#16X3+0:2]-[#7X3:3]-[#7X2:4]', [[0.345751490622808, 0.0, 1.0]]], + ['[*:1]~[#16X4,#16X3+0:2]=,:[#7X2:3]-,:[*:4]', [[2.3793383120041223, 0.0, 1.0]]], + ['[#6X4:1]-[#16X4,#16X3+0:2]-[#7X2:3]~[#6X3:4]', [[-0.17964657572853626, 0.0, 6.0], [0.22812323473297264, 0.0, 5.0], [-0.02804117546933119, 0.0, 4.0], [2.5210252903977888, 0.0, 3.0], [4.805033211757329, 3.141592653589793, 2.0], [3.6151024220381833, 0.0, 1.0]]], + ['[#8X1:1]~[#16X4,#16X3+0:2]-[#7X2:3]~[#6X3:4]', [[2.6581688346595045, 0.0, 6.0], [-0.1706543759726188, 0.0, 5.0], [2.4564590851264168, 3.141592653589793, 4.0], [4.191442986225968, 3.141592653589793, 2.0], [4.4837694424904155, 3.141592653589793, 3.0], [7.818751577525376, 0.0, 1.0]]], + ['[*:1]~[#16X4,#16X3+0:2]-[#8X2:3]-[*:4]', [[0.4154650285149811, 0.0, 1.0]]], + ['[*:1]-[#16X2,#16X3+1:2]-[#16X2,#16X3+1:3]-[*:4]', [[14.856732567584544, 0.0, 2.0], [2.2855899852506005, 0.0, 3.0]]], + ['[*:1]-[#8X2:2]-[#15:3]~[*:4]', [[2.4881935327567084, 0.0, 3.0]]], + ['[#8X2:1]-[#15:2]-[#8X2:3]-[#6X4:4]', [[-2.875084920799118, 0.0, 3.0], [-3.333702826212749, 0.0, 2.0]]], + ['[*:1]~[#7X3:2]-[#15:3]~[*:4]', [[7.44621251089932, 3.141592653589793, 2.0]]], + ['[*:1]-[#7:2]-[#15:3]=[*:4]', [[10.400389563596944, 3.141592653589793, 2.0], [1.8055038180227658, 0.0, 3.0]]], + ['[#6X3:1]-[#7:2]-[#15:3]=[*:4]', [[-1.993630627863016, 0.0, 1.0]]], + ['[*:1]~[#7:2]=[#15:3]~[*:4]', [[-3.965550516287916, 0.0, 3.0]]], + ['[*:1]-[*:2]#[*:3]-[*:4]', [[0.0, 0.0, 1.0]]], + ['[*:1]~[*:2]-[*:3]#[*:4]', [[0.0, 0.0, 1.0]]], + ['[*:1]~[*:2]=[#6,#7,#16,#15;X2:3]=[*:4]', [[0.0, 0.0, 1.0]]]]}}