11
11
import multiprocessing
12
12
import os
13
13
import subprocess
14
+ from collections .abc import Callable
14
15
from shutil import which
16
+ from tempfile import TemporaryDirectory
15
17
from typing import Any
16
18
17
19
from jobflow import job
20
+ from pydantic import BaseModel
18
21
from pymatgen .core import Structure
19
22
20
23
logger = logging .getLogger (__name__ )
21
24
25
+ _installed_extra = {"mofid" : True }
26
+ try :
27
+ from mofid .run_mofid import cif2mofid
28
+ except ImportError :
29
+ _installed_extra ["mofid" ] = False
30
+
31
+
32
+ class MofIdEntry (BaseModel ):
33
+ """
34
+ Interface for running MOFid calculations.
35
+
36
+ This class wraps the mofid executable to extract key MOF components.
37
+ """
38
+
39
+ smiles : str | None = None
40
+ Topology : str | None = None
41
+ SmilesLinkers : list [str ] | None = None
42
+ SmilesNodes : list [str ] | None = None
43
+ Mofkey : str | None = None
44
+ Mofid : str | None = None
45
+
46
+ @classmethod
47
+ def from_structure (cls , structure : Structure , ** kwargs ) -> "MofIdEntry" :
48
+ """
49
+ Run MOFid, `cif2mofid` function, in a temporary directory.
50
+
51
+ Store MOFid information: MOF topology, linker and metal nodes SMILES.
52
+ """
53
+ if not _installed_extra ["mofid" ]:
54
+ logger .debug ("MOFid not found, skipping MOFid analysis." )
55
+ return cls ()
56
+ old_cwd = os .getcwd ()
57
+ try :
58
+ with TemporaryDirectory () as tmp :
59
+ os .chdir (tmp )
60
+ structure .to ("tmp.cif" )
61
+ mofid_out = cif2mofid ("tmp.cif" , ** kwargs )
62
+ except Exception as exc : # noqa: BLE001
63
+ logger .warning ("MOFid failed: %s" , exc )
64
+ return cls ()
65
+ os .chdir (old_cwd )
66
+
67
+ remap = {
68
+ "Smiles" : "smiles" ,
69
+ "Topology" : "topology" ,
70
+ "SmilesLinkers" : "smiles_linkers" ,
71
+ "SmilesNodes" : "smiles_nodes" ,
72
+ "MofKey" : "mofkey" ,
73
+ "MofId" : "mofid" ,
74
+ }
75
+ return cls (** {k : mofid_out .get (v ) for k , v in remap .items ()})
76
+
22
77
23
78
class ZeoPlusPlus :
24
79
"""
25
80
Interface for running zeo++ calculations for MOF or zeolites.
26
81
27
82
This class wraps the zeo++ executable to calculate pore properties
83
+ (e.g, Probe-occupiable volume, Pore diameters - see zeoplusplus.org)
28
84
using given sorbate species.
29
85
"""
30
86
@@ -347,9 +403,10 @@ def run_zeopp_assessment(
347
403
sorbates : list [str ] | str | None = None ,
348
404
cif_name : str | None = None ,
349
405
nproc : int = 1 ,
406
+ rules : dict [str , Callable [[dict [str , Any ]], bool ]] | None = None ,
350
407
) -> dict [str , Any ]:
351
408
"""
352
- Run zeo++ MOF assessment on a structure.
409
+ Run zeo++ on a structure with user-defined rules .
353
410
354
411
Parameters
355
412
----------
@@ -365,11 +422,38 @@ def run_zeopp_assessment(
365
422
Filename for the CIF if structure is a Structure.
366
423
nproc : int, optional
367
424
Number of processes to use.
425
+ rules : dict[str, Callable[[dict[str, Any]], bool]], optional
426
+ Mapping of names to functions that take the full output dict
427
+ and return True/False if the structure passes each rule.
368
428
369
429
Returns
370
430
-------
371
431
dict[str, Any]
372
- Dictionary containing zeo++ outputs for each sorbate and a flag 'is_mof'.
432
+ Zeo++ outputs (per sorbate) and boolean result for the rule.
433
+
434
+ Examples
435
+ --------
436
+ Example of custom rules to assess a candidate MOF structure:
437
+
438
+ ```python
439
+ from atomate2.common.jobs.mof import run_zeopp_assessment
440
+
441
+
442
+ def custom_mof_rule(out):
443
+ props = out["N2"]
444
+ keys = ["PLD", "POAV_A^3", "PONAV_A^3"]
445
+ if not all(k in props for k in keys):
446
+ return False
447
+ return props["PLD"] > 3.0
448
+
449
+
450
+ response = run_zeopp_assessment(
451
+ structure=my_struct,
452
+ sorbates="N2",
453
+ rules={"is_mof": custom_mof_rule},
454
+ )
455
+ # response.output["is_mof"] will be True/False
456
+ ```
373
457
"""
374
458
if sorbates is None :
375
459
sorbates = ["N2" , "CO2" , "H2O" ]
@@ -399,23 +483,11 @@ def run_zeopp_assessment(
399
483
for sorbate in maker .sorbates :
400
484
output [sorbate ].update (maker .output [sorbate ])
401
485
402
- output ["is_mof" ] = False
403
- if all (
404
- k in output ["N2" ]
405
- for k in (
406
- "PLD" ,
407
- "POAV_A^3" ,
408
- "PONAV_A^3" ,
409
- "POAV_Volume_fraction" ,
410
- "PONAV_Volume_fraction" ,
411
- )
412
- ):
413
- output ["is_mof" ] = (
414
- output ["N2" ]["PLD" ] > 2.5
415
- and output ["N2" ]["POAV_Volume_fraction" ] > 0.3
416
- and output ["N2" ]["POAV_A^3" ] > output ["N2" ]["PONAV_A^3" ]
417
- and output ["N2" ]["POAV_Volume_fraction" ]
418
- > output ["N2" ]["PONAV_Volume_fraction" ]
419
- )
486
+ if rules is not None :
487
+ for name , rule_func in rules .items ():
488
+ try :
489
+ output [name ] = bool (rule_func (output ))
490
+ except Exception as e : # noqa: BLE001
491
+ output [name ] = f"rule_error: { e !s} "
420
492
421
493
return output
0 commit comments