Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
53 changes: 53 additions & 0 deletions web-conexs-api/src/web_conexs_api/jobfilebuilders.py
Original file line number Diff line number Diff line change
@@ -1,9 +1,13 @@
import io
import logging
from pathlib import Path
from typing import List

import numpy as np
from pymatgen.analysis.graphs import StructureGraph
from pymatgen.analysis.local_env import JmolNN
from pymatgen.core import Lattice, Structure
from pymatgen.io.cif import CifParser

from .models.models import (
ChemicalSite,
Expand All @@ -20,6 +24,8 @@
from .models.models import Lattice as CrystalLattice
from .periodictable import elements, periodic_table_by_z

logger = logging.getLogger(__name__)


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

return s


def cif_string_to_molecule(cif_string: str):
try:
cif_file = io.StringIO(cif_string)
parser = CifParser(cif_file)
structure = parser.parse_structures(primitive=True)
structure = structure[0]
sg = StructureGraph.from_local_env_strategy(structure, JmolNN())
my_molecules = sg.get_subgraphs_as_molecules()

if len(my_molecules) == 0 or len(my_molecules[0]) == 0:
return None

new_sites = []

for i, s in enumerate(my_molecules[0]):
new_sites.append(
ChemicalSite(
element_z=s.specie.number,
x=s.x,
y=s.y,
z=s.z,
index=i,
)
)

molecule: ChemicalStructure = ChemicalStructure(
label="Generated from cif",
sites=new_sites,
)
return molecule
except Exception as e:
logger.exception(f"Could not parse cif to molecule {e}")
return None


def cif_string_to_crystal(cif_string: str):
try:
cif_file = io.StringIO(cif_string)
parser = CifParser(cif_file)
structure = parser.parse_structures(primitive=True)
structure = structure[0]
print(structure.sites[0].species.elements[0])
return pymatstruct_to_crystal(structure, label="Generated from cif")
except Exception as e:
logger.exception(f"Could not parse cif to crystal {e}")
35 changes: 34 additions & 1 deletion web-conexs-api/src/web_conexs_api/routers/structures.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
from typing import List

from fastapi import APIRouter, Depends
from fastapi import APIRouter, Depends, HTTPException, Request
from sqlmodel import Session

from ..auth import get_current_user
Expand All @@ -10,6 +10,7 @@
upload_structure,
)
from ..database import get_session
from ..jobfilebuilders import cif_string_to_crystal, cif_string_to_molecule
from ..models.models import (
CrystalStructure,
CrystalStructureInput,
Expand Down Expand Up @@ -49,3 +50,35 @@ def get_structure_list_endpoint(
) -> List[StructureWithMetadata]:
output = get_structures(session, user_id, type)
return output


@router.post("/convert/molecule")
async def convert_to_molecule(
request: Request,
user_id: str = Depends(get_current_user),
) -> MolecularStructureInput:
body = await request.body()
molecule = cif_string_to_molecule(body.decode())

if molecule is None:
raise HTTPException(
status_code=422, detail="Could not extract structure from file"
)

return molecule


@router.post("/convert/crystal")
async def convert_to_crystal(
request: Request,
user_id: str = Depends(get_current_user),
) -> CrystalStructureInput:
body = await request.body()
crystal = cif_string_to_crystal(body.decode())

if not crystal:
raise HTTPException(
status_code=422, detail="Could not extract structure from file"
)

return crystal
22 changes: 22 additions & 0 deletions web-conexs-api/tests/test_cif_to_structure.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
from web_conexs_api.jobfilebuilders import cif_string_to_crystal, cif_string_to_molecule

cif_h_string = (
"data_1\n\n_symmetry_space_group_name_H-M P-1\n"
+ "loop_\n_symmetry_equiv_pos_site_id\n_symmetry_equiv_pos_as_xyz\n1 +x,+y,+z\n\n"
+ "_cell_angle_alpha 90\n_cell_angle_beta 90\n_cell_angle_gamma 90\n "
+ "_cell_length_a 4.1043564\n_cell_length_b 4.1043564"
+ "\n_cell_length_c 4.1043564\n\n\n"
+ "loop_\n_atom_site_label\n_atom_site_type_symbol\n_atom_site_fract_x\n"
+ "_atom_site_fract_y\n_atom_site_fract_z\nH_0 H 0 0 0\nH_1 H 0.1 0.1 0.1"
)


def test_cif_string_to_molecule():
molecule = cif_string_to_molecule(cif_h_string)
assert len(molecule.sites) == 2


def test_cif_string_to_crystal():
crystal = cif_string_to_crystal(cif_h_string)
print(crystal)
assert crystal.lattice.alpha == 90
3 changes: 0 additions & 3 deletions web-conexs-api/tests/test_jobfile_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ def test_orca_scf_filebuilder():

jobfile = build_orca_input_file(test_model, test_structure)

print(jobfile)

assert "P_ReducedOrbPopMO_L" in jobfile
assert "!ReducedPop UNO" in jobfile

Expand Down Expand Up @@ -174,7 +172,6 @@ def test_qe_filebuilder():
assert "nat = 2" in jobfile
assert "ntyp = 2" in jobfile

print(jobfile)
jobfile_array = jobfile.split("\n")

species_line = -1
Expand Down
112 changes: 112 additions & 0 deletions web-conexs-client/src/components/ConvertFromCif.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import {
Button,
Card,
CircularProgress,
Stack,
Typography,
} from "@mui/material";
import VisuallyHiddenInput from "./VisuallyHiddenInput";
import { useMutation } from "@tanstack/react-query";
import { postConvertCrystal, postConvertMolecule } from "../queryfunctions";
import { CrystalInput, MoleculeInput } from "../models";
import { useState } from "react";
import CheckCircleIcon from "@mui/icons-material/CheckCircle";
import ErrorIcon from "@mui/icons-material/Error";
import GrainIcon from "./icons/GrainIcon";
import MoleculeIcon from "./icons/MoleculeIcon";

function getConvertIcon(state: string, molecule: boolean) {
if (state == "ok") {
return <CheckCircleIcon />;
} else if (state == "failed") {
return <ErrorIcon />;
} else if (state == "running") {
return <CircularProgress size="1em" />;
}

return molecule ? <MoleculeIcon /> : <GrainIcon />;
}

export default function ConvertFromCif(props: {
isFractional: boolean;
setStructure: (moleculeInput: MoleculeInput | CrystalInput | null) => void;
}) {
const [textFile, setTextFile] = useState<string | null>(null);
const [filename, setFileName] = useState<string | null>(null);

const [convertState, setConvertState] = useState<
"default" | "ok" | "failed" | "running"
>("default");

const mutation = useMutation({
mutationFn: props.isFractional ? postConvertCrystal : postConvertMolecule,
onSuccess: (data) => {
props.setStructure(data);
setConvertState("ok");
setTimeout(() => setConvertState("default"), 2000);
},
onError: () => {
setConvertState("failed");
setTimeout(() => setConvertState("default"), 2000);
},
});

// mutation.data
const handleFile = (event: React.ChangeEvent<HTMLInputElement>) => {
if (!event.target.files) return;
const file = event.target.files[0];
setFileName(file.name);

if (file.name.endsWith(".cif")) {
const reader = new FileReader();
reader.onload = function () {
const content = reader.result as string;

setTextFile(content);
};
reader.readAsText(file); // Read the file as text
}
};

const title = props.isFractional
? "Convert from Crystal Cif File"
: "Convert from Molecular Crystal Cif File";

return (
<Card>
<Stack>
<Typography>{title}</Typography>
<Stack direction="row" spacing="5px" margin="5px">
<Button
variant="contained"
type="submit"
role={undefined}
tabIndex={-1}
component="label"
>
Upload
<VisuallyHiddenInput
type="file"
name="file1"
onChange={handleFile}
/>
</Button>
<Button
variant="outlined"
disabled={textFile == null}
onClick={() => {
if (textFile != null) {
setConvertState("running");
mutation.mutate(textFile);
}
}}
endIcon={getConvertIcon(convertState, !props.isFractional)}
>
Run Convert
</Button>
<Typography>{filename ? filename : "No file"}</Typography>
</Stack>
</Stack>
</Card>
);
}
15 changes: 15 additions & 0 deletions web-conexs-client/src/components/VisuallyHiddenInput.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { styled } from "@mui/material/styles";

const VisuallyHiddenInput = styled("input")({
clip: "rect(0 0 0 0)",
clipPath: "inset(50%)",
height: 1,
overflow: "hidden",
position: "absolute",
bottom: 0,
left: 0,
whiteSpace: "nowrap",
width: 1,
});

export default VisuallyHiddenInput;
54 changes: 35 additions & 19 deletions web-conexs-client/src/components/XYZEditor.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { TextField } from "@mui/material";
import { Stack, TextField } from "@mui/material";
import { useState } from "react";
import { validateMoleculeData } from "../utils";
import { inputToXYZNoHeader, validateMoleculeData } from "../utils";
import ConvertFromCif from "./ConvertFromCif";

export default function XYZEditor(props: {
structure: string;
Expand All @@ -26,22 +27,37 @@ export default function XYZEditor(props: {
};

return (
<TextField
error={errorMessage.length != 0}
sx={{ width: "100%" }}
id="datafilebox"
label={
props.isFractional
? "Atomic Coordinates (Fractional)"
: "Atomic Coordinates (Angstroms)"
}
rows={12}
multiline
value={data}
helperText={errorMessage}
onChange={(e) => {
onChange(e.target.value);
}}
/>
<Stack spacing="10px">
<TextField
error={errorMessage.length != 0}
sx={{ width: "100%" }}
id="datafilebox"
label={
props.isFractional
? "Atomic Coordinates (Fractional)"
: "Atomic Coordinates (Angstroms)"
}
rows={12}
multiline
value={data}
helperText={errorMessage}
onChange={(e) => {
onChange(e.target.value);
}}
/>

<ConvertFromCif
setStructure={(structure) => {
if (structure == null) {
return;
}
const s = inputToXYZNoHeader(structure);
props.setStructure(s);
setData(s);
setErrorMessage("");
}}
isFractional={props.isFractional}
></ConvertFromCif>
</Stack>
);
}
26 changes: 14 additions & 12 deletions web-conexs-client/src/components/crystals/CreateCrystalPage.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -66,23 +66,25 @@ export default function CreateCystalPage() {
Create Crystal
</Typography>
</Toolbar>
<Stack direction={"row"} spacing="5px" margin="5px">
<CrystalEditor crystal={crystal} setCrystal={setCrytal} />
<Stack direction={"row"} margin="10px">
<Stack spacing="10px" margin="10px">
<CrystalEditor crystal={crystal} setCrystal={setCrytal} />
<Button
variant="contained"
onClick={() => {
if (crystal != null) {
mutation.mutate(crystal);
}
}}
>
Create
</Button>
</Stack>
<MolStarCrystalWrapper
cif={crystal == null ? null : crystalInputToCIF(crystal)}
labelledAtomIndex={undefined}
/>
</Stack>
<Button
variant="contained"
onClick={() => {
if (crystal != null) {
mutation.mutate(crystal);
}
}}
>
Create
</Button>
</Stack>
</MainPanel>
);
Expand Down
Loading