diff --git a/ipsuite/analysis/model.py b/ipsuite/analysis/model.py index 2a25311c..a887064e 100644 --- a/ipsuite/analysis/model.py +++ b/ipsuite/analysis/model.py @@ -1,6 +1,7 @@ import contextlib import logging import pathlib +import typing import ase import matplotlib.pyplot as plt @@ -399,28 +400,21 @@ class BoxScaleAnalysis(base.ProcessSingleAtom): Attributes ---------- model: The MLModel node that implements the 'predict' method - atoms: list[Atoms] to predict properties for - logspace: bool, default=True - Increase the stdev of rattle with 'np.logspace' instead of 'np.linspace' stop: float, default = 1.0 The stop value for the generated space of stdev points num: int, default = 100 The size of the generated space of stdev points - factor: float, default = 0.001 - The 'np.linspace(0.0, stop, num) * factor' - atom_id: int, default = 0 - The atom to pick from self.atoms as a starting point start: int, default = None The initial box scale, default value is the original box size. """ model: models.MLModel = zntrack.zn.deps() - logspace: bool = zntrack.zn.params(False) - stop: float = zntrack.zn.params(2.0) - factor: float = zntrack.zn.params(1.0) + mapping: typing.Callable = zntrack.zn.deps(None) + + start: float = zntrack.zn.params(0.9) + stop: float = zntrack.zn.params(2.5) num: int = zntrack.zn.params(100) - start: float = zntrack.zn.params(None) energies: pd.DataFrame = zntrack.zn.plots( # x="x", @@ -429,35 +423,40 @@ class BoxScaleAnalysis(base.ProcessSingleAtom): # y_label="predicted energy", ) - def post_init(self): + figure = zntrack.dvc.outs(zntrack.nwd / "box_scale_analysis.png") + + def _post_init_(self): self.data = utils.helpers.get_deps_if_node(self.data, "atoms") - if self.start is None: - self.start = 0.0 if self.logspace else 1.0 def run(self): - if self.logspace: - scale_space = ( - np.logspace(start=self.start, stop=self.stop, num=self.num) * self.factor - ) - else: - scale_space = ( - np.linspace(start=self.start, stop=self.stop, num=self.num) * self.factor - ) + scale_space = np.linspace(start=self.start, stop=self.stop, num=self.num) atoms = self.get_data() cell = atoms.copy().cell - atoms.calc = self.model.calc + calc = self.model.calc energies = [] self.atoms = [] for scale in tqdm.tqdm(scale_space, ncols=70): atoms.set_cell(cell=cell * scale, scale_atoms=True) - energies.append(atoms.get_potential_energy()) - self.atoms.append(atoms.copy()) + if self.mapping is not None: + new_atoms = self.mapping({self.data_id: atoms})[0].copy() + else: + new_atoms = atoms.copy() + new_atoms.calc = calc + energies.append(new_atoms.get_potential_energy()) + + self.atoms.append(new_atoms) self.energies = pd.DataFrame({"y": energies, "x": scale_space}) + fig, ax = plt.subplots() + ax.plot(scale_space, energies) + ax.set_ylabel("Predicted Energy (eV)") + ax.set_xlabel("Scale factor of the initial cell") + fig.savefig(self.figure, bbox_inches="tight") + class BoxHeatUp(base.ProcessSingleAtom): """Attributes diff --git a/ipsuite/fields/__init__.py b/ipsuite/fields/__init__.py index 199bd122..a338554b 100644 --- a/ipsuite/fields/__init__.py +++ b/ipsuite/fields/__init__.py @@ -1,5 +1,6 @@ """Custom ZnTrack serialization types.""" from ipsuite.fields.atoms import Atoms +from ipsuite.fields.graph import NxGraph -__all__ = ["Atoms"] +__all__ = ["Atoms", "NxGraph"] diff --git a/ipsuite/fields/atoms.py b/ipsuite/fields/atoms.py index 0b833487..e7152a1e 100644 --- a/ipsuite/fields/atoms.py +++ b/ipsuite/fields/atoms.py @@ -26,7 +26,10 @@ def get_stage_add_argument(self, instance: zntrack.Node) -> typing.List[tuple]: def save(self, instance: zntrack.Node): """Save value with ase.db.connect.""" - atoms: base.ATOMS_LST = getattr(instance, self.name) + try: + atoms: base.ATOMS_LST = getattr(instance, self.name) + except AttributeError: + return instance.nwd.mkdir(exist_ok=True, parents=True) file = self.get_files(instance)[0] diff --git a/ipsuite/fields/graph.py b/ipsuite/fields/graph.py new file mode 100644 index 00000000..937e4313 --- /dev/null +++ b/ipsuite/fields/graph.py @@ -0,0 +1,40 @@ +"""Lazy ASE Atoms loading.""" +import json +import pathlib +import typing + +import networkx as nx +import zntrack + + +class NxGraph(zntrack.Field): + """Store list[ase.Atoms] in an ASE database.""" + + dvc_option = "--outs" + group = zntrack.FieldGroup.RESULT + + def __init__(self): + super().__init__(use_repr=False) + + def get_files(self, instance: zntrack.Node) -> list: + return [(instance.nwd / f"{self.name}.json").as_posix()] + + def get_stage_add_argument(self, instance: zntrack.Node) -> typing.List[tuple]: + return [(self.dvc_option, file) for file in self.get_files(instance)] + + def save(self, instance: zntrack.Node): + """Save value with ase.db.connect.""" + try: + graph: nx.Graph = getattr(instance, self.name) + except AttributeError: + return + instance.nwd.mkdir(exist_ok=True, parents=True) + file = self.get_files(instance)[0] + with pathlib.Path(file).open("w") as f: + json.dump(graph, f, default=nx.node_link_data) + + def get_data(self, instance: zntrack.Node) -> typing.List[nx.Graph]: + """Get graph File.""" + file = self.get_files(instance)[0] + with pathlib.Path(file).open("r") as f: + return [nx.node_link_graph(x) for x in json.load(f)] diff --git a/ipsuite/testing/__init__.py b/ipsuite/testing/__init__.py new file mode 100644 index 00000000..685d2220 --- /dev/null +++ b/ipsuite/testing/__init__.py @@ -0,0 +1,147 @@ +import os +import pathlib +import typing + +import ase.io +import dvc.cli +import git +import numpy as np +import zntrack +from ase.calculators.singlepoint import SinglePointCalculator +from zntrack.tools import timeit + +from ipsuite import AddData, Project, base, fields + + +class UpdateCalculator(base.ProcessSingleAtom): + """Update the calculator of an atoms object. + + Set energy, forces to zero. + """ + + energy = zntrack.zn.params(0.0) + forces = zntrack.zn.params((0, 0, 0)) + + time: float = zntrack.zn.metrics() + + @timeit(field="time") + def run(self) -> None: + self.atoms = self.get_data() + + self.atoms.calc = SinglePointCalculator( + self.atoms, + energy=self.energy, + forces=np.stack([self.forces] * len(self.atoms)), + ) + self.atoms = [self.atoms] + + +class MockAtoms(zntrack.Node): + """Create Atoms objects with random data.""" + + atoms: typing.List[ase.Atoms] = fields.Atoms() + seed: int = zntrack.zn.params(0) + + n_configurations: int = zntrack.zn.params(10) + n_atoms: int = zntrack.zn.params(10) + + calculator: bool = zntrack.zn.params(True) + + def run(self) -> None: + self.atoms = [] + np.random.seed(self.seed) + for _ in range(self.n_configurations): + atoms = ase.Atoms( + symbols="C" * self.n_atoms, + positions=np.random.random((self.n_atoms, 3)), + ) + if self.calculator: + atoms.calc = SinglePointCalculator( + atoms, + energy=np.random.random(), + forces=np.random.random((self.n_atoms, 3)), + ) + self.atoms.append(atoms) + + +class AtomsToXYZ(base.AnalyseAtoms): + """Convert Atoms objects to XYZ files.""" + + output: pathlib.Path = zntrack.dvc.outs(zntrack.nwd / "atoms") + + def run(self) -> None: + self.output.mkdir(parents=True, exist_ok=True) + for idx, atom in enumerate(self.data): + ase.io.write(self.output / f"{idx:05d}.xyz", atom) + + @property + def files(self) -> typing.List[pathlib.Path]: + return [x.resolve() for x in self.output.glob("*.xyz")] + + +class NodesPerAtoms(base.ProcessAtoms): + processor: base.ProcessSingleAtom = zntrack.zn.nodes() + repo: str = zntrack.meta.Text(None) + commit: bool = zntrack.meta.Text(True) + clean_exp: bool = zntrack.meta.Text(True) + + def run(self): + # lazy loading: load now + _ = self.data + processor = self.processor + processor.name = processor.__class__.__name__ + + repo = git.Repo.init(self.repo or self.name) + + gitignore = pathlib.Path(".gitignore") + # TODO: move this into a function + if not gitignore.exists(): + gitignore.write_text(f"{repo.working_dir}\n") + elif repo.working_dir not in gitignore.read_text().split(" "): + gitignore.write_text(f"{repo.working_dir}\n") + + os.chdir(repo.working_dir) + dvc.cli.main(["init"]) + project = Project() + + with project: + data = AddData(file="atoms.xyz") + project.run(repro=False) + + processor.data = data @ "atoms" + processor.write_graph() + + repo.git.add(all=True) + repo.index.commit("Build graph") + + if self.clean_exp: + dvc.cli.main(["exp", "gc", "-w", "-f"]) + + self.run_exp(project, processor) + if self.commit: + self.run_commits(repo) + + os.chdir("..") # we need to go back to save + + def run_exp(self, project, processor): + exp_lst = [] + for atom in self.data: + with project.create_experiment() as exp: + ase.io.write("atoms.xyz", atom) + exp_lst.append(exp) + project.run_exp() + + self.atoms = [ + processor.from_rev(name=processor.name, rev=x.name).atoms[0] for x in exp_lst + ] + + def run_commits(self, repo): + commits = [] + for idx, atom in enumerate(self.data): + ase.io.write("atoms.xyz", atom) + dvc.cli.main(["add", "atoms.xyz"]) + dvc.cli.main(["repro"]) + repo.git.add(all=True) + # do not use repo.index.add("*"); it will add atoms.xyz + commit_message = f"repro {self.name}_{idx}" + commits.append(repo.index.commit(commit_message)) diff --git a/ipsuite/testing/mapping.py b/ipsuite/testing/mapping.py new file mode 100644 index 00000000..f558ea09 --- /dev/null +++ b/ipsuite/testing/mapping.py @@ -0,0 +1,274 @@ +"""Molecule Mapping using smiles and networkx""" + + +import contextlib +import typing + +import ase +import networkx as nx +import numpy as np +import tqdm +import zntrack +from ase.calculators.singlepoint import PropertyNotImplementedError, SinglePointCalculator +from ase.constraints import FixBondLengths +from rdkit import Chem +from rdkit.Chem import AllChem + +from ipsuite import base, fields + + +class MoleculeMapping(base.ProcessAtoms): + smiles: str = zntrack.zn.params() + n_molecules: int = zntrack.zn.params() + threshold: float = zntrack.zn.params(2.0) + skip_errors: bool = zntrack.zn.params(False) + + # TODO remove n_molecules + # TODO make smiles a dict {ratio: smiles} + # where ratio can be used e.g. MgCl2 would be {1: "Mg", 2: "Cl"} + # or a list if equal ratios + + graphs = fields.NxGraph() + + def get_allowed_bonds(self) -> dict: + """Get allowed bonds for each atom type + + + Returns + ------- + dict: + Dictionary of allowed bonds for each atom type. + An example of the dictionary is given below. + The first key is the atom type and the second key is a dictionary + of allowed bonds for that atom type with the maximum number of bonds. + { + 'C': {'C': 2, 'H': 3, 'N': 2}, + 'N': {'C': 3}, + 'B': {'F': 4}, + 'F': {'B': 1}, + 'H': {'C': 1} + } + + """ + mol = Chem.MolFromSmiles(self.smiles) + mol = AllChem.AddHs(mol) + + bonds = {} + + for i in range(mol.GetNumAtoms()): + atom = mol.GetAtomWithIdx(i) + if atom.GetSymbol() not in bonds: + bonds[atom.GetSymbol()] = {} + neighbors = atom.GetNeighbors() + symbols = [neighbor.GetSymbol() for neighbor in neighbors] + for element in set(symbols): + count = bonds[atom.GetSymbol()].get(element, 0) + if symbols.count(element) > count: + bonds[atom.GetSymbol()][element] = symbols.count(element) + + return bonds + + def get_graph(self, atoms, bonds) -> nx.Graph: + """Get a graph from atoms + + Parameters + ---------- + atoms : ase.Atoms + Atoms to generate a graph from + threshold : float + Threshold for the distance between atoms to be considered a bond + count : int + Number of isolated molecules. + bonds : dict + Dictionary of allowed bonds for each atom type. + """ + sizes = [] + thresholds = [] + threshold = self.threshold + for _ in range(100): # 100 trials + distances = atoms.get_all_distances() + graph = nx.Graph() + for idx, _ in enumerate(atoms): + graph.add_node(idx) + + np.fill_diagonal(distances, np.inf) + # TODO do it per symbol and max neighbors + symbols = np.array(atoms.get_chemical_symbols()) + for symbol in set(symbols): + # only consider allowed bonds via smiles + possible_dij = np.array(distances.copy()) + possible_dij[symbols != symbol] = np.inf + possible_dij[:, ~np.isin(symbols, list(bonds[symbol]))] = np.inf + graph.add_edges_from( + [ + (x[0].item(), x[1].item()) + for x in np.argwhere(possible_dij < threshold) + ] + ) + size = len(list(nx.connected_components(graph))) + sizes.append(size) + thresholds.append(threshold) + if size == self.n_molecules: + return graph + elif size > self.n_molecules: + threshold += 0.01 + else: + threshold -= 0.01 + raise ValueError("Could not generate a graph.") + + def run(self) -> None: + self.graphs = [] + bonds = self.get_allowed_bonds() + + for atoms in tqdm.tqdm(self.get_data()): + try: + self.graphs.append(self.get_graph(atoms, bonds)) + except Exception as e: + if not self.skip_errors: + raise e + self.graphs.append(self.graphs[-1]) # TODO this is not true! + print(e) + self.atoms = self.get_coarse_grained_atoms() + + def get_rdkit_molecules(self, item=None): + if item is None: + item = slice(None) + molecules = [] + + for graph, ase_atoms in zip( + self.graphs[item], self.get_data()[item], strict=True + ): + for mol_ids in nx.connected_components(graph): + mol_graph = graph.subgraph(mol_ids) + mol = Chem.RWMol() + + for node in mol_graph: + ase_atom = ase_atoms[node] + atom = Chem.Atom(ase_atom.symbol) + atom.SetDoubleProp("x", ase_atom.position[0]) + atom.SetDoubleProp("y", ase_atom.position[1]) + atom.SetDoubleProp("z", ase_atom.position[2]) + mol.AddAtom(atom) + + mapping = {idx: val for val, idx in enumerate(mol_graph.nodes)} + mol_graph_relabled = nx.relabel_nodes(mol_graph, mapping, copy=True) + for edge in mol_graph_relabled.edges: + mol.AddBond(int(edge[0]), int(edge[1]), Chem.BondType.SINGLE) + + molecules.append(mol) + return molecules + + @property + def atoms_fixed_interatomic_bonds(self) -> base.protocol.ATOMS_LST: + atoms_lst = [] + for atoms, graph in zip(self.get_data(), self.graphs, strict=True): + atoms.set_constraint(FixBondLengths(np.array(graph.edges))) + atoms_lst.append(atoms) + + return atoms_lst + + def get_molecule_ids(self, graph) -> typing.List[typing.List[int]]: + """Get molecule ids from a graph.""" + return [list(mol_ids) for mol_ids in nx.connected_components(graph)] + + def get_molecules(self, graph, atoms: ase.Atoms) -> typing.List[ase.Atoms]: + """Get molecules from a graph.""" + molecules = [] + for mol_ids in self.get_molecule_ids(graph): + atomic_numbers = np.array(atoms.get_atomic_numbers())[mol_ids] + positions = np.array(atoms.get_positions())[mol_ids] + calc = None + mol = ase.Atoms( + atomic_numbers, + positions=positions, + cell=atoms.get_cell(), + pbc=atoms.get_pbc(), + ) + if atoms.calc: + with contextlib.suppress(PropertyNotImplementedError): + forces = np.array(atoms.get_forces())[mol_ids] + calc = SinglePointCalculator(mol, forces=forces) + mol.calc = calc + molecules.append(mol) + return molecules + + def get_com(self, atoms: ase.Atoms) -> np.ndarray: + """Get center of mass of atoms. + + Try to accoung for pbc. + """ + atoms = atoms.copy() + reference = atoms.get_positions(wrap=True)[0] + atoms.center() + atoms.wrap() + + shift = atoms.get_positions(wrap=True)[0] - reference + return atoms.get_center_of_mass() - shift + + def get_coarse_grained_atoms(self) -> base.protocol.ATOMS_LST: + """Get coarse grained atoms by COM of molecules.""" + cc_atoms_list = [] + for graph, atoms in zip(self.graphs, self.get_data(), strict=True): + coms = [self.get_com(mol) for mol in self.get_molecules(graph, atoms)] + cc_atoms_list.append( + ase.Atoms(positions=coms, cell=atoms.get_cell(), pbc=atoms.get_pbc()) + ) + return cc_atoms_list + + def get_all_atoms(self, data): + """Get all atoms by COM of molecules. + + Parameters + ---------- + data : list|dict of ase.Atoms + List or dict of coarse grained atoms. + if provides as list: + The atom ids and length must the same as the + output generated by 'get_coarse_grained_atoms'. + if provided as dict: + the keys must be the 0based index of the atoms + in the output generated by 'get_coarse_grained_atoms'. + Positions and cell size can be different. + + Returns + ------- + new_atoms : list of ase.Atoms + List of all atoms based on the COM of the data input. + This will only affect the positions of the atoms. + Rotations will be kept the same as in 'self.get_data()'. + """ + + # TODO a simple test: coarse grain and then undo and check if the same + # currently distances in pbc are the same but not the positions + new_atoms = [] + + if isinstance(data, list): + data = dict(enumerate(data)) + + self.update_data() + + for idx in data: + graph = self.graphs[idx] + atoms = self.data[idx] + cc_atoms = data[idx] + + new_atomic_numbers, new_positions = [], [] + for mol, com in zip( + self.get_molecules(graph, atoms), cc_atoms.get_positions(), strict=True + ): + mol.positions -= self.get_com(mol) + # mol.wrap() # TODO this is not working + + new_atomic_numbers.append(mol.get_atomic_numbers()) + new_positions.append(mol.get_positions() + com) + + new_atoms.append( + ase.Atoms( + np.concatenate(new_atomic_numbers), + positions=np.concatenate(new_positions), + cell=cc_atoms.get_cell(), + pbc=cc_atoms.get_pbc(), + ) + ) + # new_atoms[-1].wrap() + return new_atoms diff --git a/poetry.lock b/poetry.lock index 30fc2755..520aebdb 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1550,14 +1550,14 @@ tests = ["mypy (==0.971)", "pylint (==2.15.0)", "pytest (==7.2.0)", "pytest-cov [[package]] name = "dvc-render" -version = "0.3.0" +version = "0.3.1" description = "DVC render" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "dvc-render-0.3.0.tar.gz", hash = "sha256:dc13ab3c3e7dc225019e60fb2d3b2501ff3f402ec64b66b99f2dbc95306aa4c8"}, - {file = "dvc_render-0.3.0-py3-none-any.whl", hash = "sha256:6fedd7e11422dde701e469ea7c72053801c16002dc5ba9e9151fab270f135a27"}, + {file = "dvc-render-0.3.1.tar.gz", hash = "sha256:af718d813017db9b2e688576f33a8ce27398949620340894afae4f1023f0e07a"}, + {file = "dvc_render-0.3.1-py3-none-any.whl", hash = "sha256:98667e4290bab07bb9c404e796a212d41408d70e8094ed9b73166176788de944"}, ] [package.extras] @@ -1766,14 +1766,14 @@ psutil = ">=5.9.0" [[package]] name = "fonttools" -version = "4.39.1" +version = "4.39.2" description = "Tools to manipulate font files" category = "main" optional = false python-versions = ">=3.8" files = [ - {file = "fonttools-4.39.1-py3-none-any.whl", hash = "sha256:54c871c0587c1f3ad3a71b390e503edbb4638d90ad14f905f2fa5938a72fc76b"}, - {file = "fonttools-4.39.1.zip", hash = "sha256:51c72f26a5b79fc77d59f22e9d146350fbd1d107430ddda7b7665b1be38b16ad"}, + {file = "fonttools-4.39.2-py3-none-any.whl", hash = "sha256:85245aa2fd4cf502a643c9a9a2b5a393703e150a6eaacc3e0e84bb448053f061"}, + {file = "fonttools-4.39.2.zip", hash = "sha256:e2d9f10337c9e3b17f9bce17a60a16a885a7d23b59b7f45ce07ea643e5580439"}, ] [package.extras] @@ -2445,19 +2445,19 @@ test = ["codecov", "coverage", "ipykernel (>=6.14)", "mypy", "paramiko", "pre-co [[package]] name = "jupyter-core" -version = "5.2.0" +version = "5.3.0" description = "Jupyter core package. A base package on which Jupyter projects rely." category = "dev" optional = false python-versions = ">=3.8" files = [ - {file = "jupyter_core-5.2.0-py3-none-any.whl", hash = "sha256:4bdc2928c37f6917130c667d8b8708f20aee539d8283c6be72aabd2a4b4c83b0"}, - {file = "jupyter_core-5.2.0.tar.gz", hash = "sha256:1407cdb4c79ee467696c04b76633fc1884015fa109323365a6372c8e890cc83f"}, + {file = "jupyter_core-5.3.0-py3-none-any.whl", hash = "sha256:d4201af84559bc8c70cead287e1ab94aeef3c512848dde077b7684b54d67730d"}, + {file = "jupyter_core-5.3.0.tar.gz", hash = "sha256:6db75be0c83edbf1b7c9f91ec266a9a24ef945da630f3120e1a0046dc13713fc"}, ] [package.dependencies] platformdirs = ">=2.5" -pywin32 = {version = ">=1.0", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} +pywin32 = {version = ">=300", markers = "sys_platform == \"win32\" and platform_python_implementation != \"PyPy\""} traitlets = ">=5.3" [package.extras] @@ -4607,6 +4607,44 @@ ase = ">=3.17.0" f90wrap = ">=0.2.6" numpy = ">=1.13" +[[package]] +name = "rdkit" +version = "2022.9.5" +description = "A collection of chemoinformatics and machine-learning software written in C++ and Python" +category = "main" +optional = false +python-versions = "*" +files = [ + {file = "rdkit-2022.9.5-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ba416aea1ecaf96c856227263e763e9b51261aaba348feea4015d16932a3229d"}, + {file = "rdkit-2022.9.5-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4324b703ac44d8486a911c3313a79df6d75b04ede54c62dbf611d0212658eac5"}, + {file = "rdkit-2022.9.5-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c8b168214684589a243acb5be44e2d6fb35eb710e05a58aa27c6a449e912dac9"}, + {file = "rdkit-2022.9.5-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:072c6c72c09a9ecc02a9df11adc4500005f883878c4146846bfc33c2739cf2de"}, + {file = "rdkit-2022.9.5-cp310-cp310-win_amd64.whl", hash = "sha256:a8598d78a8b43295fb5ae3b137f7311c21ea395cab32894b5d3b98f93446c049"}, + {file = "rdkit-2022.9.5-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:7e56bbc104342c534268daaa1b096f51fb855c50a6d24e3cec8506b6c5b02502"}, + {file = "rdkit-2022.9.5-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c7881f240e283300e3ef53830d73635a9898db0cc70860737545465662787e6b"}, + {file = "rdkit-2022.9.5-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e39e29300fc68d53b2d2db63816f5c347d13bd8478afe4763140d416dec62a54"}, + {file = "rdkit-2022.9.5-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4a0904c8d4e2a8fa34d8732b14b1dbe223b54094daa69dc0040bede6bdeebfa5"}, + {file = "rdkit-2022.9.5-cp311-cp311-win_amd64.whl", hash = "sha256:984be71bf3ab0e84b4cf208a296a19e1ae1bc34d5d33fdc47a4446e5de9a8ecb"}, + {file = "rdkit-2022.9.5-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:51ead2e112aa086cf411f84b0d072432771def958db25253d10dadc52e46fe52"}, + {file = "rdkit-2022.9.5-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8c8c19ea0d893b4935fc61e767b7e63d56e1a04f057719117af2ec6a861e03af"}, + {file = "rdkit-2022.9.5-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c60060b2b6439b9c6f2ec2c95e0b43a693534896fa0f2e143034ea64a9a18e05"}, + {file = "rdkit-2022.9.5-cp37-cp37m-win_amd64.whl", hash = "sha256:f8042828e1287c474b839f540abd4567b4f73c375becd39e62ec631e05e41c5e"}, + {file = "rdkit-2022.9.5-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:a180b0db174a905b256006ff7bf200274d690b53ab0d96341d1a9d9742c02656"}, + {file = "rdkit-2022.9.5-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:9b97c3a57d3a436c355c1a6206088b2a1c29c80e232fd47fad14a888627316b9"}, + {file = "rdkit-2022.9.5-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:6e665e5002486e16acec094666b09f77703ad6bf2f517e089e993a3ede20ec11"}, + {file = "rdkit-2022.9.5-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17578b01a24d8cb792d5f1a5c62dd65be60eb09bcf240eb18cf862f1841346d"}, + {file = "rdkit-2022.9.5-cp38-cp38-win_amd64.whl", hash = "sha256:f47888fbc0c36a92fbfa7fe917d27f79600e7434a4ada3160dfe8a1ccd3af8ed"}, + {file = "rdkit-2022.9.5-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:9470447bf9581b06403144240d59a2ff92abdbdf6f023424e175c091781338c0"}, + {file = "rdkit-2022.9.5-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:fd2741108757252e862442b1b2a3fc9965b6ecb3c75c87f1797efa073e1ddb10"}, + {file = "rdkit-2022.9.5-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:96f8a904874edc9497d504ede827fdab10783b3d56b7dceefe9ecb66c8539cb3"}, + {file = "rdkit-2022.9.5-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4cc6135ff385f6b1ef2f110931e16a53d7b73205d89eec0d32363d8ed9251fb1"}, + {file = "rdkit-2022.9.5-cp39-cp39-win_amd64.whl", hash = "sha256:25f035e07dc179cd41763ed6d52aaed04ff6125e2315ee69b3f982c29757a40f"}, +] + +[package.dependencies] +numpy = "*" +Pillow = "*" + [[package]] name = "regex" version = "2021.11.10" @@ -6170,4 +6208,4 @@ nequip = ["nequip"] [metadata] lock-version = "2.0" python-versions = ">=3.10,<3.12" -content-hash = "69e06ddbdfba0c8039c277bc08b1851b6b5bef10314ce825991f0fa4f2239611" +content-hash = "e61dbea41a64c73a9c5eb7a09f400a03452545272e09b604d18bf45199c4e762" diff --git a/pyproject.toml b/pyproject.toml index 0facb8cc..afd4a9af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -35,6 +35,7 @@ xmltodict = "^0.13.0" nequip = { version = "^0.5.6", optional = true } +rdkit = "^2022.9.5" [tool.poetry.group.allegro.dependencies] mir-allegro = { git = "https://github.com/mir-group/allegro.git" }