Skip to content

Commit ab572da

Browse files
authored
add ability to upload a cif to be converted (#59)
* add ability to upload a cif to be converted
1 parent c21dbc5 commit ab572da

File tree

14 files changed

+411
-68
lines changed

14 files changed

+411
-68
lines changed

web-conexs-api/src/web_conexs_api/jobfilebuilders.py

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,13 @@
11
import io
2+
import logging
23
from pathlib import Path
34
from typing import List
45

56
import numpy as np
7+
from pymatgen.analysis.graphs import StructureGraph
8+
from pymatgen.analysis.local_env import JmolNN
69
from pymatgen.core import Lattice, Structure
10+
from pymatgen.io.cif import CifParser
711

812
from .models.models import (
913
ChemicalSite,
@@ -20,6 +24,8 @@
2024
from .models.models import Lattice as CrystalLattice
2125
from .periodictable import elements, periodic_table_by_z
2226

27+
logger = logging.getLogger(__name__)
28+
2329

2430
def sites_to_string(sites: List[ChemicalSite], use_symbol=True, absorbing_index=None):
2531
structure_string = ""
@@ -411,3 +417,50 @@ def pymatstruct_to_crystal(structure: Structure, label="materials project struct
411417
s = CrystalStructureInput(label=label, sites=site_list, lattice=lattice)
412418

413419
return s
420+
421+
422+
def cif_string_to_molecule(cif_string: str):
423+
try:
424+
cif_file = io.StringIO(cif_string)
425+
parser = CifParser(cif_file)
426+
structure = parser.parse_structures(primitive=True)
427+
structure = structure[0]
428+
sg = StructureGraph.from_local_env_strategy(structure, JmolNN())
429+
my_molecules = sg.get_subgraphs_as_molecules()
430+
431+
if len(my_molecules) == 0 or len(my_molecules[0]) == 0:
432+
return None
433+
434+
new_sites = []
435+
436+
for i, s in enumerate(my_molecules[0]):
437+
new_sites.append(
438+
ChemicalSite(
439+
element_z=s.specie.number,
440+
x=s.x,
441+
y=s.y,
442+
z=s.z,
443+
index=i,
444+
)
445+
)
446+
447+
molecule: ChemicalStructure = ChemicalStructure(
448+
label="Generated from cif",
449+
sites=new_sites,
450+
)
451+
return molecule
452+
except Exception as e:
453+
logger.exception(f"Could not parse cif to molecule {e}")
454+
return None
455+
456+
457+
def cif_string_to_crystal(cif_string: str):
458+
try:
459+
cif_file = io.StringIO(cif_string)
460+
parser = CifParser(cif_file)
461+
structure = parser.parse_structures(primitive=True)
462+
structure = structure[0]
463+
print(structure.sites[0].species.elements[0])
464+
return pymatstruct_to_crystal(structure, label="Generated from cif")
465+
except Exception as e:
466+
logger.exception(f"Could not parse cif to crystal {e}")

web-conexs-api/src/web_conexs_api/routers/structures.py

Lines changed: 34 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
from typing import List
22

3-
from fastapi import APIRouter, Depends
3+
from fastapi import APIRouter, Depends, HTTPException, Request
44
from sqlmodel import Session
55

66
from ..auth import get_current_user
@@ -10,6 +10,7 @@
1010
upload_structure,
1111
)
1212
from ..database import get_session
13+
from ..jobfilebuilders import cif_string_to_crystal, cif_string_to_molecule
1314
from ..models.models import (
1415
CrystalStructure,
1516
CrystalStructureInput,
@@ -49,3 +50,35 @@ def get_structure_list_endpoint(
4950
) -> List[StructureWithMetadata]:
5051
output = get_structures(session, user_id, type)
5152
return output
53+
54+
55+
@router.post("/convert/molecule")
56+
async def convert_to_molecule(
57+
request: Request,
58+
user_id: str = Depends(get_current_user),
59+
) -> MolecularStructureInput:
60+
body = await request.body()
61+
molecule = cif_string_to_molecule(body.decode())
62+
63+
if molecule is None:
64+
raise HTTPException(
65+
status_code=422, detail="Could not extract structure from file"
66+
)
67+
68+
return molecule
69+
70+
71+
@router.post("/convert/crystal")
72+
async def convert_to_crystal(
73+
request: Request,
74+
user_id: str = Depends(get_current_user),
75+
) -> CrystalStructureInput:
76+
body = await request.body()
77+
crystal = cif_string_to_crystal(body.decode())
78+
79+
if not crystal:
80+
raise HTTPException(
81+
status_code=422, detail="Could not extract structure from file"
82+
)
83+
84+
return crystal
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
from web_conexs_api.jobfilebuilders import cif_string_to_crystal, cif_string_to_molecule
2+
3+
cif_h_string = (
4+
"data_1\n\n_symmetry_space_group_name_H-M P-1\n"
5+
+ "loop_\n_symmetry_equiv_pos_site_id\n_symmetry_equiv_pos_as_xyz\n1 +x,+y,+z\n\n"
6+
+ "_cell_angle_alpha 90\n_cell_angle_beta 90\n_cell_angle_gamma 90\n "
7+
+ "_cell_length_a 4.1043564\n_cell_length_b 4.1043564"
8+
+ "\n_cell_length_c 4.1043564\n\n\n"
9+
+ "loop_\n_atom_site_label\n_atom_site_type_symbol\n_atom_site_fract_x\n"
10+
+ "_atom_site_fract_y\n_atom_site_fract_z\nH_0 H 0 0 0\nH_1 H 0.1 0.1 0.1"
11+
)
12+
13+
14+
def test_cif_string_to_molecule():
15+
molecule = cif_string_to_molecule(cif_h_string)
16+
assert len(molecule.sites) == 2
17+
18+
19+
def test_cif_string_to_crystal():
20+
crystal = cif_string_to_crystal(cif_h_string)
21+
print(crystal)
22+
assert crystal.lattice.alpha == 90

web-conexs-api/tests/test_jobfile_builder.py

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,8 +42,6 @@ def test_orca_scf_filebuilder():
4242

4343
jobfile = build_orca_input_file(test_model, test_structure)
4444

45-
print(jobfile)
46-
4745
assert "P_ReducedOrbPopMO_L" in jobfile
4846
assert "!ReducedPop UNO" in jobfile
4947

@@ -174,7 +172,6 @@ def test_qe_filebuilder():
174172
assert "nat = 2" in jobfile
175173
assert "ntyp = 2" in jobfile
176174

177-
print(jobfile)
178175
jobfile_array = jobfile.split("\n")
179176

180177
species_line = -1
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
import {
2+
Button,
3+
Card,
4+
CircularProgress,
5+
Stack,
6+
Typography,
7+
} from "@mui/material";
8+
import VisuallyHiddenInput from "./VisuallyHiddenInput";
9+
import { useMutation } from "@tanstack/react-query";
10+
import { postConvertCrystal, postConvertMolecule } from "../queryfunctions";
11+
import { CrystalInput, MoleculeInput } from "../models";
12+
import { useState } from "react";
13+
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
14+
import ErrorIcon from "@mui/icons-material/Error";
15+
import GrainIcon from "./icons/GrainIcon";
16+
import MoleculeIcon from "./icons/MoleculeIcon";
17+
18+
function getConvertIcon(state: string, molecule: boolean) {
19+
if (state == "ok") {
20+
return <CheckCircleIcon />;
21+
} else if (state == "failed") {
22+
return <ErrorIcon />;
23+
} else if (state == "running") {
24+
return <CircularProgress size="1em" />;
25+
}
26+
27+
return molecule ? <MoleculeIcon /> : <GrainIcon />;
28+
}
29+
30+
export default function ConvertFromCif(props: {
31+
isFractional: boolean;
32+
setStructure: (moleculeInput: MoleculeInput | CrystalInput | null) => void;
33+
}) {
34+
const [textFile, setTextFile] = useState<string | null>(null);
35+
const [filename, setFileName] = useState<string | null>(null);
36+
37+
const [convertState, setConvertState] = useState<
38+
"default" | "ok" | "failed" | "running"
39+
>("default");
40+
41+
const mutation = useMutation({
42+
mutationFn: props.isFractional ? postConvertCrystal : postConvertMolecule,
43+
onSuccess: (data) => {
44+
props.setStructure(data);
45+
setConvertState("ok");
46+
setTimeout(() => setConvertState("default"), 2000);
47+
},
48+
onError: () => {
49+
setConvertState("failed");
50+
setTimeout(() => setConvertState("default"), 2000);
51+
},
52+
});
53+
54+
// mutation.data
55+
const handleFile = (event: React.ChangeEvent<HTMLInputElement>) => {
56+
if (!event.target.files) return;
57+
const file = event.target.files[0];
58+
setFileName(file.name);
59+
60+
if (file.name.endsWith(".cif")) {
61+
const reader = new FileReader();
62+
reader.onload = function () {
63+
const content = reader.result as string;
64+
65+
setTextFile(content);
66+
};
67+
reader.readAsText(file); // Read the file as text
68+
}
69+
};
70+
71+
const title = props.isFractional
72+
? "Convert from Crystal Cif File"
73+
: "Convert from Molecular Crystal Cif File";
74+
75+
return (
76+
<Card>
77+
<Stack>
78+
<Typography>{title}</Typography>
79+
<Stack direction="row" spacing="5px" margin="5px">
80+
<Button
81+
variant="contained"
82+
type="submit"
83+
role={undefined}
84+
tabIndex={-1}
85+
component="label"
86+
>
87+
Upload
88+
<VisuallyHiddenInput
89+
type="file"
90+
name="file1"
91+
onChange={handleFile}
92+
/>
93+
</Button>
94+
<Button
95+
variant="outlined"
96+
disabled={textFile == null}
97+
onClick={() => {
98+
if (textFile != null) {
99+
setConvertState("running");
100+
mutation.mutate(textFile);
101+
}
102+
}}
103+
endIcon={getConvertIcon(convertState, !props.isFractional)}
104+
>
105+
Run Convert
106+
</Button>
107+
<Typography>{filename ? filename : "No file"}</Typography>
108+
</Stack>
109+
</Stack>
110+
</Card>
111+
);
112+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
import { styled } from "@mui/material/styles";
2+
3+
const VisuallyHiddenInput = styled("input")({
4+
clip: "rect(0 0 0 0)",
5+
clipPath: "inset(50%)",
6+
height: 1,
7+
overflow: "hidden",
8+
position: "absolute",
9+
bottom: 0,
10+
left: 0,
11+
whiteSpace: "nowrap",
12+
width: 1,
13+
});
14+
15+
export default VisuallyHiddenInput;
Lines changed: 35 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
import { TextField } from "@mui/material";
1+
import { Stack, TextField } from "@mui/material";
22
import { useState } from "react";
3-
import { validateMoleculeData } from "../utils";
3+
import { inputToXYZNoHeader, validateMoleculeData } from "../utils";
4+
import ConvertFromCif from "./ConvertFromCif";
45

56
export default function XYZEditor(props: {
67
structure: string;
@@ -26,22 +27,37 @@ export default function XYZEditor(props: {
2627
};
2728

2829
return (
29-
<TextField
30-
error={errorMessage.length != 0}
31-
sx={{ width: "100%" }}
32-
id="datafilebox"
33-
label={
34-
props.isFractional
35-
? "Atomic Coordinates (Fractional)"
36-
: "Atomic Coordinates (Angstroms)"
37-
}
38-
rows={12}
39-
multiline
40-
value={data}
41-
helperText={errorMessage}
42-
onChange={(e) => {
43-
onChange(e.target.value);
44-
}}
45-
/>
30+
<Stack spacing="10px">
31+
<TextField
32+
error={errorMessage.length != 0}
33+
sx={{ width: "100%" }}
34+
id="datafilebox"
35+
label={
36+
props.isFractional
37+
? "Atomic Coordinates (Fractional)"
38+
: "Atomic Coordinates (Angstroms)"
39+
}
40+
rows={12}
41+
multiline
42+
value={data}
43+
helperText={errorMessage}
44+
onChange={(e) => {
45+
onChange(e.target.value);
46+
}}
47+
/>
48+
49+
<ConvertFromCif
50+
setStructure={(structure) => {
51+
if (structure == null) {
52+
return;
53+
}
54+
const s = inputToXYZNoHeader(structure);
55+
props.setStructure(s);
56+
setData(s);
57+
setErrorMessage("");
58+
}}
59+
isFractional={props.isFractional}
60+
></ConvertFromCif>
61+
</Stack>
4662
);
4763
}

web-conexs-client/src/components/crystals/CreateCrystalPage.tsx

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -66,23 +66,25 @@ export default function CreateCystalPage() {
6666
Create Crystal
6767
</Typography>
6868
</Toolbar>
69-
<Stack direction={"row"} spacing="5px" margin="5px">
70-
<CrystalEditor crystal={crystal} setCrystal={setCrytal} />
69+
<Stack direction={"row"} margin="10px">
70+
<Stack spacing="10px" margin="10px">
71+
<CrystalEditor crystal={crystal} setCrystal={setCrytal} />
72+
<Button
73+
variant="contained"
74+
onClick={() => {
75+
if (crystal != null) {
76+
mutation.mutate(crystal);
77+
}
78+
}}
79+
>
80+
Create
81+
</Button>
82+
</Stack>
7183
<MolStarCrystalWrapper
7284
cif={crystal == null ? null : crystalInputToCIF(crystal)}
7385
labelledAtomIndex={undefined}
7486
/>
7587
</Stack>
76-
<Button
77-
variant="contained"
78-
onClick={() => {
79-
if (crystal != null) {
80-
mutation.mutate(crystal);
81-
}
82-
}}
83-
>
84-
Create
85-
</Button>
8688
</Stack>
8789
</MainPanel>
8890
);

0 commit comments

Comments
 (0)