Skip to content

Commit a8ac85c

Browse files
committed
Added commandline tool for runing prediction
1 parent 98320db commit a8ac85c

8 files changed

Lines changed: 171 additions & 24 deletions

File tree

.gitignore

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -246,4 +246,7 @@ __pycache__/
246246
.pytest_cache/
247247
.ipynb_checkpoints/
248248
examples/create_metal_salts.py
249-
examples/test.xyz
249+
examples/test.xyz
250+
examples/iupacname_smiles.msgpack
251+
fairmofsyncondition/call_model/mof_prediction.txt
252+
examples/combine_all_names2.py

README.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,22 @@ pip install fairmofsyncondition
5353

5454
## Useful tool
5555

56+
`fairmofsyncondition_syncon`
57+
is a command-line tool for predicting synthetic conditions of Metal–Organic Frameworks (MOFs) directly from CIF files.
58+
It extracts **organic ligands**, **space group information**, and computes the **top-5 predicted metal salts**.
59+
60+
Quickly run command on any cif file
61+
62+
```bash
63+
fairmofsyncondition_syncon .my_mof.cif
64+
```
65+
66+
Or run and provide and outfile
67+
68+
```bash
69+
fairmofsyncondition_syncon my_mof.cif -o my_mof_report.txt
70+
```
71+
5672
`iupac2cheminfor`
5773
one of the most useful tool is to directly extract cheminonformatic identifiers such
5874
as inchikey and smile strings directly from iupac names or common names. This can be

docs/source/index.rst

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,21 @@ To install `fairmofsyncondition` from PYPI, simply execute the following command
4545
4646
pip install fairmofsyncondition
4747
48+
49+
Predict Synthesis Conditions
50+
============================
51+
52+
The quickest way to predict the synthesis condition from any crystal structure is by
53+
running the below commandline argument.
54+
55+
.. code-block:: bash
56+
fairmofsyncondition_syncon my_mof.cif
57+
58+
.. code-block:: bash
59+
fairmofsyncondition_syncon my_mof.cif -o my_mof_report.txt
60+
61+
62+
4863
License
4964
=======
5065

fairmofsyncondition/call_model/run_model.py

Lines changed: 123 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
from fairmofsyncondition.read_write import filetyper, coords_library
1515
from fairmofsyncondition.call_model.utils_model import get_models, ensemble_predictions
1616

17+
smile_to_iupac = filetyper.smile_names_iupac()
1718

1819
convert_struct = {'cubic': 0,
1920
'hexagonal': 1,
@@ -26,36 +27,37 @@
2627

2728
device = "cpu"
2829

29-
def get_ligand_iupacname(ligand_inchi):
30+
def get_ligand_iupacname(ligand_inchi, smiles):
3031
name = load_iupac_names().get(ligand_inchi, None)
31-
print("tesst", name)
3232

3333
if name is None:
34-
pubchem = cheminfo2iupac.pubchem_to_inchikey(ligand_inchi, name='inchikey')
34+
name = smile_to_iupac.get(smiles, None)
35+
if name is None:
36+
pubchem = cheminfo2iupac.pubchem_to_inchikey(smiles, name='smiles')
3537
if pubchem is None:
36-
name = ligand_inchi
38+
name = None
3739
else:
38-
pubchem.get('iupac_name', ligand_inchi)
40+
name = pubchem.get('iupac_name', None)
3941
return name
4042

4143
class Synconmodel(object):
4244
def __init__(self,
4345
ase_atom,
44-
outfile="mof_prediction.txt",
45-
module="best_model.pth",
46+
outfile="mof_prediction.txt"
4647
):
4748
"""
4849
A class to run the metal salt prediction GNN model and for
4950
extracting organic ligands and predicting metal salts.
5051
"""
5152
if isinstance(ase_atom, Atoms):
5253
self.ase_atom = ase_atom
54+
self.basename = 'MOF Data'
5355
elif isinstance(ase_atom, str) and os.path.isfile(ase_atom):
5456
self.ase_atom = read(ase_atom)
57+
self.basename = os.path.basename(ase_atom).split('.')[0]
5558
else:
5659
print("sorry input type is not recorgnised")
5760
self.outfile = outfile
58-
self.gnn_model = module
5961
self.structure = MOFstructure(self.ase_atom)
6062
self.pymat = AseAtomsAdaptor.get_structure(self.ase_atom)
6163
self.torch_data = coords_library.ase_to_pytorch_geometric(self.ase_atom)
@@ -103,11 +105,12 @@ def get_coordination_and_oms(self):
103105
return emb, oms, metals
104106

105107
def get_organic_ligands(self):
108+
ligands_data = {}
106109
_, ligands = self.structure.get_ligands()
107110
inchikeys = [ligand.info.get('inchikey') for ligand in ligands]
108-
ligands_names = [get_ligand_iupacname(i) for i in inchikeys]
109-
print(ligands_names)
110-
111+
smiles = [ligand.info.get('smi') for ligand in ligands]
112+
ligands_names = [get_ligand_iupacname(i, j) for i, j in zip(inchikeys, smiles)]
113+
return inchikeys, smiles, ligands_names
111114

112115
def get_space_group(self):
113116
"Get space group embedding"
@@ -118,13 +121,13 @@ def get_space_group(self):
118121
emb_sg[space_group_number] = 1
119122
get_crystal_system = sga.get_crystal_system()
120123
emb_cs[convert_struct[get_crystal_system]] = 1
121-
return emb_sg, emb_cs
124+
return emb_sg, emb_cs, space_group_number, get_crystal_system
122125

123126
def complete_torch_data(self):
124127
'''
125128
Get torch data
126129
'''
127-
emb_sg, emb_cs = self.get_space_group()
130+
emb_sg, emb_cs, _, _ = self.get_space_group()
128131
cn_emb, oms, metals = self.get_coordination_and_oms()
129132
atom_conc = self.get_species_conc()
130133
self.torch_data.atomic_one_hot = atom_conc
@@ -134,27 +137,124 @@ def complete_torch_data(self):
134137
self.torch_data.oms = torch.tensor([[oms]], dtype=torch.float)
135138
return self.torch_data.to(device), metals
136139

137-
138140
def predict_condition(self):
139141
'''
140142
predict conditions
141143
'''
142144
torch_data, metals = self.complete_torch_data()
143145
torch_data = torch_data.to(device)
144-
torch_data.cordinates = torch_data.cordinates[0] # small cleaning
145-
models = get_models(torch_data, device=device) # load models (5 seeds)
146+
torch_data.cordinates = torch_data.cordinates[0]
147+
models = get_models(torch_data, device=device)
146148
models = models[0:1]
147149

148150
category_names = filetyper.category_names()["metal_salts"]
149-
# the function to get ensemble predictions, i.e. the average probability for each class over the 5 models
150151
pred_list = ensemble_predictions(models, torch_data, category_names, device=device)
151-
152-
for name, prob in pred_list[:10]:
153-
print(f"{name}: {prob:.3f}")
152+
# for name, prob in pred_list[:10]:
153+
# print(f"{name}: {prob:.3f}")
154154

155155
#print(pred_list)
156+
return pred_list
157+
158+
def compile_data(self):
159+
data = []
160+
inchikeys, smiles, ligands_names = self.get_organic_ligands()
161+
_, _, space_group_number, get_crystal_system = self.get_space_group()
162+
pred_list = self.predict_condition()[:5]
163+
164+
data.append("\n")
165+
data.append(f"Predicted Synthetic Data Report\n")
166+
data.append(f"For: {self.basename}\n")
167+
data.append("=" * 80 + "\n")
168+
data.append(f"{'Space group number:':25} {space_group_number}\n")
169+
data.append(f"{'Crystal system:':25} {get_crystal_system}\n\n")
170+
171+
172+
data.append("Organic Ligands\n")
173+
data.append("-" * 80 + "\n")
174+
data.append(f"{'InChIKey':<28} {'SMILES':<30} {'IUPAC Name':<20}\n")
175+
data.append("-" * 80 + "\n")
176+
for inchi, smi, iupac in zip(inchikeys, smiles, ligands_names):
177+
inchi_str = str(inchi) if inchi is not None else "N/A"
178+
smi_str = str(smi) if smi is not None else "N/A"
179+
iupac_str = str(iupac) if iupac is not None else "N/A"
180+
data.append(f"{inchi_str:<28} {smi_str:<30} {iupac_str:<20}\n")
181+
data.append("\n")
182+
183+
184+
data.append("Top 5 Predicted Metal Salts\n")
185+
data.append("-" * 80 + "\n")
186+
data.append(f"{'Metal Salt':<40} {'Probability':>15}\n")
187+
data.append("-" * 80 + "\n")
188+
for metal_salt, prob in pred_list:
189+
salt_str = str(metal_salt) if metal_salt is not None else "N/A"
190+
prob_str = f"{prob:.4f}" if prob is not None else "N/A"
191+
data.append(f"{salt_str:<40} {prob_str:>15}\n")
192+
data.append("=" * 80 + "\n")
193+
194+
data.append("\n")
195+
data.append("Report generated by fairmofsyncondition\n")
196+
data.append("Authors: Dinga Wonanke & Antonio Longa\n")
197+
data.append("=" * 80 + "\n")
198+
199+
200+
filetyper.put_contents(self.outfile, data)
201+
202+
203+
def print_helpful_information():
204+
'''
205+
Prints helpful information about using the fairmofsyncondition script.
206+
'''
207+
help_text = """
208+
Usage: fairmofsyncondition.py [CIF_FILE] [OPTIONS]
209+
210+
This script predicts synthetic conditions for MOFs given a CIF file
211+
or any ase readable fileformat.,
212+
extracts organic ligand information, space group, and top predicted
213+
metal salts, and writes a formatted report to a text file.
214+
215+
Positional Arguments:
216+
CIF_FILE Path to the input CIF file.
217+
218+
Optional Arguments:
219+
-o, --output FILE The path to the output report file (default: prediction_report.txt).
220+
221+
Examples:
222+
fairmofsyncondition_syncon my_mof.cif -o my_mof_report.txt
223+
fairmofsyncondition_syncon ../data/sample.cif
224+
"""
225+
print(help_text)
226+
227+
def main():
228+
parser = argparse.ArgumentParser(
229+
description="Predict synthetic conditions for MOFs from CIF files."
230+
)
231+
parser.add_argument(
232+
'cif_file',
233+
type=str,
234+
nargs='?',
235+
default=None,
236+
help='Path to the CIF file to analyze.'
237+
)
238+
parser.add_argument(
239+
'-o', '--output',
240+
type=str,
241+
default='mof_prediction_report.txt',
242+
help='Path to the output report file (default: prediction_report.txt).'
243+
)
244+
245+
args = parser.parse_args()
246+
247+
if args.cif_file is None:
248+
print_helpful_information()
249+
sys.exit(1)
156250

251+
try:
252+
ml_data = Synconmodel(args.cif_file)
253+
ml_data.outfile = args.output
254+
ml_data.compile_data()
255+
except Exception as e:
256+
print(f"Error: {e}")
257+
sys.exit(1)
157258

158-
ml_data = Synconmodel('../../tests/test_data/EDUSIF.cif')
159-
# print(ml_data.predict_condition())
160-
print(ml_data.get_organic_ligands())
259+
if __name__ == '__main__':
260+
main()

fairmofsyncondition/db/iupacname_smiles.msgpack

Lines changed: 1 addition & 0 deletions
Large diffs are not rendered by default.

fairmofsyncondition/read_write/cheminfo2iupac.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,12 @@ def pubchem_to_inchikey(identifier, name='smiles'):
4040
otherwise, None.
4141
'''
4242
properties = {}
43+
4344
pubchem = pcp.get_compounds(identifier, name)
45+
# print('inchikey', pubchem[0].inchikey)
46+
# print('name', pubchem[0].synonyms)
47+
# print('iupac', pubchem[0].iupac_name)
48+
# print('isomeric_smile', pubchem[0].isomeric_smiles)
4449
if len(pubchem) > 0:
4550
all_prop = pubchem[0]
4651
properties['inchikey'] = all_prop.inchikey if all_prop.inchikey else None

fairmofsyncondition/read_write/filetyper.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -560,3 +560,9 @@ def category_names():
560560
"load category"
561561
msgpack_path = files("fairmofsyncondition").joinpath("db/category_names.msgpack")
562562
return load_data(msgpack_path)
563+
564+
565+
def smile_names_iupac():
566+
"load iupac"
567+
msgpack_path = files("fairmofsyncondition").joinpath("db/iupacname_smiles.msgpack")
568+
return load_data(msgpack_path)

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,6 +42,7 @@ find_bde_parameters = "fairmofsyncondition.model.hyper_optimiser:main"
4242
iupac2cheminfor = "fairmofsyncondition.read_write.iupacname2cheminfo:main"
4343
cheminfor2iupac = "fairmofsyncondition.read_write.cheminfo2iupac:main"
4444
struct2iupac = "fairmofsyncondition.read_write.struct2iupac:main"
45+
fairmofsyncondition_syncon = "fairmofsyncondition.call_model.run_model:main"
4546

4647

4748
[tool.poetry.group.dev.dependencies]

0 commit comments

Comments
 (0)