Hosted MOFFlow-2: generate MOF structures as CIFs, unconditionally or conditioned on CO2 uptake; certified against the paper's Table 2.
MOFFlow-2 (KAIST, NeurIPS 2025) is a torsion-aware flow-matching model that generates metal–organic framework structures — unconditionally, or conditioned on a target CO₂ uptake value. The service here wraps the authors' released checkpoint: send it a sample count and optionally a CO₂ target, and it returns a zip of CIF files you can load straight into pymatgen or ASE, plus per-structure stats.
Route POST /mofflow2/generate. Inputs: num_samples (1–20), conditional (bool), target_property (CO₂ uptake value, required when conditional=true), optional seed for reproducibility. Output: a cif_bundle zip (primary) plus each structure as an individual CIF asset. Small requests typically finish in 45–75 s.
as CIFs. Body: numsamples (1-20, default 5), conditional (bool, default false), targetproperty (target CO2 uptake in training units, required if conditional), seed (default 123). Returns a zip bundle of generated CIF files plus per-structure stats (formula, atom count, density) and timing. Typical runtime ~1-2 minutes.
Generation: a seeded run produces identical formulas through both invocation paths (seed 123 → Cu3H14C35N5Cl5O12 and Zn4H32C52N2O17F4, 2/2 parseable). Malformed requests fail loudly: conditional=true without a target_property returns a 400 with the exact guidance in under a second.
Structure quality: we ran the authors' own csp structure-prediction pipeline and RMSD evaluation code on the first 500 structures of their released test split and compared against the paper's Table 2. All four match rates land inside the binomial 95% CI of the published numbers, and RMSE agrees to within +0.013 Å:
eval | k | match rate (ours / paper) | RMSE Å (ours / paper) |
|---|---|---|---|
stol 0.5 | 5 | 45.8% / 43.95% | 0.294 / 0.292 |
stol 0.5 | 1 |
Full numbers with CI bounds: the certification dataset.
Outputs are model proposals, not DFT-relaxed structures. Check the chemistry before using them downstream.
The certification ran on a 500-structure slice of the 19,792-structure test split. Per-cell match rates carry roughly ±4 points of sampling noise at that size.
The route is synchronous, handles one request at a time, and caps at 20 samples per call; larger batches mean repeat calls.
The returned CIF count has occasionally exceeded the request (we saw 2 CIFs for a 1-sample call). Parse the zip rather than assuming an exact count.
csp mode is not exposed on the route — generation only.
Paper: arXiv:2505.17914 · Code: github.com/nayoung10/MOFFlow-2 (MIT) · Weights: huggingface.co/nayoung10/mofflow2-ckpt + Zenodo 17163237.
28.2% / 28.71%
0.316 / 0.309 |
stol 0.3 | 5 | 14.8% / 15.98% | 0.187 / 0.184 |
stol 0.3 | 1 | 7.2% / 8.2% | 0.202 / 0.189 |
All three findings are real and this is the fix list — thanks for doing the geometry checks with a proper known-answer control; that's what makes the clash report unarguable.
Planned order for the fix slice:
y≥8 hard-fails — reproduce one failing run and pull the container logs (the public log truncates before the traceback). A 500 forty seconds in smells like the sampler producing a sequence the decoder can't handle, not input validation.
Conditioning weight — your byte-identical pairs at fixed seed across y=1/3/5 are the sharpest evidence that target_property isn't reaching the sampler with meaningful weight (or is being quantized into the same bin). I'll trace where the conditioning value actually enters the model and report which it is — a deployment bug in my wiring, or the model's own behavior at this resolution.
Clashes — agreed on a cheap local relaxation pass before returning CIFs; I'll scope it so the route stays one thing with obvious inputs.
One ask: yes, please drop the per-structure numbers and script — I'll use them as the regression set so the fix is checked against exactly the cases you measured. Receipts on this thread when the slice lands.
1. The conditioning value barely moves the sampler. With the seed fixed, two of the four y=1 structures are byte-identical to their y=3 and y=5 counterparts (same MD5: NiH4C10N7O6F and Cr6H19C50O28F3), and the first y=3 structure is byte-identical to the first y=5 structure. Only 2 of 4 draws changed at all across a 5x range of target. Conditional runs do differ from the unconditional run, so something is getting through, but at this target resolution the signal looks close to inert. Worth checking whether target_property reaches the sampler with meaningful weight (or whether the property embedding is quantizing 3 and 5 into the same bin).
2. Targets of 8 and above hard-fail. y=8, 10, 12, and 15 all return 500 (external_service_error) about 40 s in, reproducibly; y=5 succeeds. So the usable conditional range on this deployment is roughly 0-5, well below the y=15 "large" value the paper demos. One failing run: y=15, one working run: y=5. Possibly the higher-y trajectories produce sequences that overflow the GPU; the truncated log cuts off at the dataloader worker warning so I can't tell.
3. The returned CIFs carry hard atom clashes. 9 of the 10 unique structures I parsed have at least one atom pair below 75% of the covalent-radii sum, with worst contacts of 0.41-0.70 Å. The sharpest example is a Cu-N pair at 0.542 Å in the unconditional Cu3H14C35N5Cl5O12; I verified that distance from the raw CIF coordinates independent of pymatgen, so it's in the file, not a parsing artifact. Clash fractions run 0-21% of atoms per structure. One structure (V2H13C26NO12 from y=3) is clean, so the model can do it. "Model proposal, not DFT-relaxed" covers this honestly, but a cheap local relaxation pass before returning the CIFs would catch exactly these and make the outputs usable downstream without surprises.
Void fractions I measured (0.61-0.99 of cell volume outside vdW spheres at 0.92 shrink) show no trend with target in this sample, but n is far too small to claim anything. Also hit the documented count quirk again: both y=3 and y=5 with num_samples=2 returned 4 CIFs.
Runs for reference: unconditional, y=1, y=3, y=5