1111import multiprocessing
1212import os
1313import subprocess
14+ from collections .abc import Callable
1415from shutil import which
16+ from tempfile import TemporaryDirectory
1517from typing import Any
1618
1719from jobflow import job
20+ from pydantic import BaseModel
1821from pymatgen .core import Structure
1922
2023logger = logging .getLogger (__name__ )
2124
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+
2277
2378class ZeoPlusPlus :
2479 """
2580 Interface for running zeo++ calculations for MOF or zeolites.
2681
2782 This class wraps the zeo++ executable to calculate pore properties
83+ (e.g, Probe-occupiable volume, Pore diameters - see zeoplusplus.org)
2884 using given sorbate species.
2985 """
3086
@@ -347,9 +403,10 @@ def run_zeopp_assessment(
347403 sorbates : list [str ] | str | None = None ,
348404 cif_name : str | None = None ,
349405 nproc : int = 1 ,
406+ rules : dict [str , Callable [[dict [str , Any ]], bool ]] | None = None ,
350407) -> dict [str , Any ]:
351408 """
352- Run zeo++ MOF assessment on a structure.
409+ Run zeo++ on a structure with user-defined rules .
353410
354411 Parameters
355412 ----------
@@ -365,11 +422,38 @@ def run_zeopp_assessment(
365422 Filename for the CIF if structure is a Structure.
366423 nproc : int, optional
367424 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.
368428
369429 Returns
370430 -------
371431 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+ ```
373457 """
374458 if sorbates is None :
375459 sorbates = ["N2" , "CO2" , "H2O" ]
@@ -399,23 +483,11 @@ def run_zeopp_assessment(
399483 for sorbate in maker .sorbates :
400484 output [sorbate ].update (maker .output [sorbate ])
401485
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} "
420492
421493 return output
0 commit comments