-- full integration with mol-edit backend
-- added readme
-- added vqe local_computational
This commit is contained in:
2026-05-25 19:17:04 +03:00
parent 76fe1c4294
commit c5ece3ff37
16 changed files with 1134 additions and 163 deletions

198
README.md Normal file
View File

@@ -0,0 +1,198 @@
# Quantum Compute Modules
Репозиторий содержит набор модулей для распределённой системы квантовых вычислений. Каждый модуль представляет собой тип эксперимента, который может быть выполнен на локальных вычислительных системах.
## Архитектура модуля
Каждый модуль состоит из двух независимых компонентов:
| Компонент | Назначение |
|-----------|------------|
| **Frontend** | Пользовательский интерфейс. React SPA, компилируется в HTML, встраивается в iframe на центральном сервере. |
| **Executor** | Python модуль, который скачивается и выполняется на локальной вычислительной системе. |
## Frontend
### Режимы работы
Frontend поддерживает два режима отображения:
| Режим | Когда используется | Что показывает |
|-------|-------------------|----------------|
| `List` | В списке задач эксперимента | Краткая информация (карточка) |
| `Editor` | На странице отдельной задачи | Полный редактор с параметрами |
### API взаимодействия (postMessage)
#### Получение данных от системы
```javascript
window.addEventListener("message", (event) => {
const { type, data } = event.data;
if (type === "plugin-data") {
// data.taskData — JSON строка с данными задачи
// data.mode — "List" или "Editor"
// data.theme — "light" или "dark"
// data.simProgress — прогресс выполнения (если есть)
}
if (type === "update") {
// Сигнал обновить UI (после сохранения)
}
});
```
#### Отправка данных в систему
```javascript
// При изменении данных задачи
window.parent.postMessage({
type: "plugin-update",
data: JSON.stringify(taskData), // Данные задачи
qubits_needed: number // Требуемое количество кубит
}, "*");
// Сигнал готовности (отправляется после загрузки)
window.parent.postMessage({ type: "plugin-ready" }, "*");
```
### Обязательные экспорты
React приложение должно экспортировать компоненты с интерфейсом:
```typescript
interface TaskData {
// Определяется для каждого модуля
}
interface TaskEditorProps {
data: TaskData | undefined;
setData: (data: TaskData, qubits_needed: number) => void;
counter: number; // Инкрементируется при обновлении данных извне
simProgress: any; // Прогресс выполнения (conn.send из executor)
}
```
### Сборка
```bash
npm run build
# Результат: dist/index.html — один файл со встроенными CSS/JS
```
## Executor
Python модуль, который скачивается с сервера и выполняется на локальной ВС. Должен содержать две функции:
### prepare_data
Валидация и предобработка данных задачи. Вызывается на сервере перед помещением задачи в очередь.
```python
def prepare_data(data: dict, system_info: dict) -> dict:
```
Результаты, возвращаемые функцией, передаются затем в run_computation.
В случае ошибки в данных, функция должны поднять ошибку.
### run_computation
Выполнение вычислений на локальной ВС.
```python
def run_computation(conn, dev, data):
"""
Args:
conn: Connection объект для отправки прогресса
conn.send(dict) — отправляет сообщение на сервер
(сообщения сохраняются в simulation_result.result_data)
dev: PennyLane устройство
qml.device(dev['name'], wires=dev['wires'])
data: processed_data из prepare_data
Returns: None
"""
```
### Пример executor'а
```python
def prepare_data(data: dict, system_info: dict) -> dict:
# Валидация входных данных
if "my_param" not in data:
return {
"valid": False,
"error_message": "Отсутствует параметр my_param"
}
return {
"valid": True,
"processed_data": data,
"qubits_required": data.get("qubits", 10)
}
def run_computation(conn, dev, data):
import numpy as np
for step in range(data.get("steps", 100)):
# ... вычисления ...
# Отправка прогресса
conn.send({
"step": step,
"current_value": current_value
})
return
```
## Создание нового модуля
### Шаг 1: Создание Frontend
1. Скопируйте шаблон `new_module/` или создайте React приложение с нуля
2. Реализуйте компоненты `ListItem` и `EditorPage`
3. Реализуйте `wrapper.tsx` с обработкой postMessage
4. Соберите проект: `npm run build`
### Шаг 2: Создание Executor
1. Создайте файл `executor.py`
2. Реализуйте функцию `prepare_data`
3. Реализуйте функцию `run_computation`
### Шаг 3: Регистрация модуля
Через API центрального сервера:
```bash
curl -X POST https://quantum-backend/experiment/types \
-F "name=Название модуля" \
-F "description=Описание модуля" \
-F "file_frontend=@dist/index.html" \
-F "file_comp_system=@executor.py"
```
## Список модулей
| Модуль | Описание | Статус |
|--------|----------|--------|
| `vqe` | Расчёт энергии основного состояния молекул | ✅ Готов |
## Шаблон нового модуля
```bash
git clone https://git.deowl.ru/vkrb/vqe_module
cd quantum-modules
cp -r new_module my_module
# Редактируем frontend и executor
# Регистрируем через API
```

View File

@@ -1,7 +1,7 @@
from collections.abc import AsyncIterator from collections.abc import AsyncIterator
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI from fastapi import FastAPI, Request
from fastapi.exceptions import HTTPException from fastapi.exceptions import HTTPException
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@@ -14,7 +14,13 @@ from logging_config import logger
from openbabel import pybel from openbabel import pybel
from pyscf import gto # pyright: ignore from pyscf import gto # pyright: ignore
from redis import asyncio as aioredis from redis import asyncio as aioredis
from request_response_models import BasicError, ConvertRequest, MolFileModel from request_response_models import (
BasicError,
ConvertRequest,
MolFileModel,
SmilesRequest,
)
from starlette.middleware.base import BaseHTTPMiddleware
@asynccontextmanager @asynccontextmanager
@@ -28,25 +34,47 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]:
app = FastAPI(lifespan=lifespan) app = FastAPI(lifespan=lifespan)
origins = [ class ForceCORSHeadersMiddleware(BaseHTTPMiddleware):
"http://localhost", async def dispatch(self, request: Request, call_next):
"http://localhost:8001", # Handle OPTIONS preflight requests
] if request.method == "OPTIONS":
response = JSONResponse(status_code=200, content={})
else:
response = await call_next(request)
app.add_middleware( # Always add CORS headers
CORSMiddleware, response.headers["Access-Control-Allow-Origin"] = "*"
allow_origins=origins, response.headers["Access-Control-Allow-Methods"] = (
allow_credentials=True, "GET, POST, PUT, DELETE, OPTIONS"
allow_methods=["*"], )
allow_headers=["*"], response.headers["Access-Control-Allow-Headers"] = "*"
) response.headers["Access-Control-Allow-Credentials"] = "true"
return response
# Add this middleware FIRST
app.add_middleware(ForceCORSHeadersMiddleware)
@app.options("/{path:path}")
async def options_handler(request: Request):
return JSONResponse(
content={},
status_code=200,
headers={
"Access-Control-Allow-Origin": "*", # or specific origins
"Access-Control-Allow-Methods": "GET, POST, PUT, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "*",
},
)
@app.post( @app.post(
"/convert", "/convert",
responses={ responses={
200: {"description": "Coversion Successful", "model": MolFileModel}, 200: {"description": "Coversion Successful", "model": MolFileModel},
400: {"description": "Item created", "model": BasicError}, 400: {"description": "Error Converting", "model": BasicError},
}, },
) )
def convert_molecule(req: ConvertRequest): def convert_molecule(req: ConvertRequest):
@@ -84,6 +112,22 @@ def convert_molecule(req: ConvertRequest):
raise HTTPException(status_code=400, detail={"error": str(e)}) raise HTTPException(status_code=400, detail={"error": str(e)})
@app.post(
"/smiles",
)
def smiles_mol(req: SmilesRequest):
try:
# Rad the string and format from request
mol = pybel.readstring("xyz", req.text)
mol.addh()
mol.OBMol.SetTitle("")
mol2string = mol.write("smi", opt={"h": True, "U": True})
return JSONResponse({"molfile": mol2string}, 200)
except Exception as e:
raise HTTPException(status_code=400, detail={"error": str(e)})
@app.get("/informats") @app.get("/informats")
@cache() @cache()
async def get_informats(): async def get_informats():

View File

@@ -9,6 +9,10 @@ class ConvertRequest(BaseModel):
optimize_geometry: bool = False optimize_geometry: bool = False
class SmilesRequest(BaseModel):
text: str
class MolFileModel(BaseModel): class MolFileModel(BaseModel):
molfile: str molfile: str

View File

@@ -1,6 +1,6 @@
<!-- index.html --> <!-- index.html -->
<!doctype html> <!doctype html>
<html> <html style="height: 100%">
<head> <head>
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" />
@@ -9,8 +9,6 @@
margin: 0; margin: 0;
padding: 0; padding: 0;
box-sizing: border-box; box-sizing: border-box;
background: none;
background-color: var(--mantine-color-secondary);
} }
body { body {
font-family: font-family:
@@ -19,7 +17,7 @@
} }
</style> </style>
</head> </head>
<body> <body style="background-color: transparent !important; height: 100%">
<div id="root"></div> <div id="root"></div>
<script type="module" src="/src/index.tsx"></script> <script type="module" src="/src/index.tsx"></script>
</body> </body>

View File

@@ -4,6 +4,7 @@ import { useEffect, useRef } from "react";
interface TextAreaProps { interface TextAreaProps {
text: string; text: string;
onTextChange: (text: string) => void; onTextChange: (text: string) => void;
enabled: boolean;
} }
export function CodeTextArea(props: TextAreaProps) { export function CodeTextArea(props: TextAreaProps) {
@@ -52,6 +53,7 @@ export function CodeTextArea(props: TextAreaProps) {
className="MoleculeEdit" className="MoleculeEdit"
value={props.text} value={props.text}
ref={inputRef} ref={inputRef}
disabled={!props.enabled}
onChange={(e) => { onChange={(e) => {
props.onTextChange(e.currentTarget.value); props.onTextChange(e.currentTarget.value);
}} }}

View File

@@ -0,0 +1,62 @@
export interface ConvertSchema {
inputText: string;
inputFormat: string;
add_h: boolean;
make_3d: boolean;
optimize: boolean;
}
export async function ConvertMoleculeToStandart(
data: ConvertSchema,
): Promise<string> {
try {
const response = await fetch("https://mol-convert.deowl.ru" + "/convert", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
text: data.inputText,
format: data.inputFormat,
convert_3d: data.make_3d,
add_hydrogen: data.add_h,
optimize_geometry: data.optimize,
}),
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const responseData = await response.json();
return responseData.molfile;
} catch (error) {
// Error handling
if (error instanceof Error) {
return error;
}
throw error;
}
}
export async function GetInFormats(): Promise<{ [key: string]: string }> {
try {
const response = await fetch(
"https://mol-convert.deowl.ru" + "/informats",
{
method: "GET",
headers: {
"Content-Type": "application/json",
},
},
);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
} catch (error) {
throw error;
}
}

View File

@@ -1,90 +1,291 @@
import { import {
Button, Button,
Card,
Center, Center,
Divider, Divider,
Tabs, Tabs,
Title,
Text, Text,
Textarea,
useMantineTheme, useMantineTheme,
Paper,
Badge,
SimpleGrid,
} from "@mantine/core"; } from "@mantine/core";
import { Group, Panel, Separator } from "react-resizable-panels"; import { Group, Panel, Separator } from "react-resizable-panels";
import "./MoleculePage.css"; import "./MoleculePage.css";
import { import {
IconCheck, IconAlertCircle,
IconChartBar,
IconCode, IconCode,
IconGripHorizontal,
IconGripVertical, IconGripVertical,
IconList, IconList,
IconX,
} from "@tabler/icons-react"; } from "@tabler/icons-react";
import { CodeTextArea } from "./CodeTextArea/CodeTextArea"; import { CodeTextArea } from "./CodeTextArea/CodeTextArea";
import MoleculeViewer from "./MoleculeViewer/MoleculeViewer"; import MoleculeViewer from "./MoleculeViewer/MoleculeViewer";
import { MoleculeData, SelectedAtom, TaskEditorProps } from "../Types/plugin"; import { SelectedAtom, TaskEditorProps } from "../Types/plugin";
import { useState } from "react"; import { useEffect, useState } from "react";
import { NewMoleculeModal } from "./NewMolecule/NewMolecule";
function MoleculeEditorPage(props: TaskEditorProps<MoleculeData>) { function MoleculeEditorPage(props: TaskEditorProps) {
const [text, setText] = useState(props.data?.data.text); const [text, setText] = useState(props.data?.text);
const [selectedAtom, setSelectedAtom] = useState<SelectedAtom | null>(null); const [selectedAtom, setSelectedAtom] = useState<SelectedAtom | null>(null);
const theme = useMantineTheme(); const theme = useMantineTheme();
const [isOpen, setIsOpen] = useState(false);
const [counter_new, set_counter_new] = useState(0);
useEffect(() => {
console.log("Resetting text");
setText(props.data?.text);
setSelectedAtom(null);
}, [props.counter]);
const get_qubits = (text: string) => {
try {
const lines = text.trim().replace("\\\\", "\\").split("\n") || [
"",
"",
"",
];
console.log(lines);
const secondLine = lines[1]; // "Charge=0 Multiplicity=1 Electrons=10 Orbitals=9"
const variables = secondLine.split(" ").reduce(
(acc, pair) => {
const [key, value] = pair.split("=");
acc[key] = parseInt(value); // or Number(value) if you want to keep as number
return acc;
},
{} as Record<string, number>,
);
return variables.Orbitals * 2;
} catch {
return 0;
}
};
return ( return (
<> <>
<div style={{ display: "flex", flexDirection: "column", height: "90vh" }}> <div
style={{
display: "flex",
flexDirection: "column",
height: props.simProgress ? "100%" : "90vh",
}}
>
<NewMoleculeModal
isOpened={isOpen}
setIsOpened={setIsOpen}
setMolecule={(data) => {
props.setData(data, get_qubits(data.text));
setText(data.text);
setTimeout(() => set_counter_new((old) => old + 2), 100);
}}
/>
{!props.simProgress && (
<Button onClick={() => setIsOpen(true)}>Импротировать</Button>
)}
<Divider style={{ margin: "10px" }} /> <Divider style={{ margin: "10px" }} />
<Group style={{ flex: 1, gap: "5px" }}> <Group orientation="vertical" style={{ flex: 1, gap: "5px" }}>
<Panel minSize={200} defaultSize={500}> <Panel>
<Tabs <Group style={{ flex: 1, gap: "5px" }}>
variant="outline" <Panel minSize={200} defaultSize={500}>
style={{ <Tabs
height: "100%", variant="outline"
display: "flex", style={{
flexDirection: "column", height: "100%",
}} display: "flex",
radius="lg" flexDirection: "column",
defaultValue="code"
classNames={{ panel: "tabPanel" }}
>
<Tabs.List>
<Tabs.Tab value="code" leftSection={<IconCode size={12} />}>
Код
</Tabs.Tab>
<Tabs.Tab value="list" leftSection={<IconList size={12} />}>
Список
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="code">
<CodeTextArea
text={text || ""}
onTextChange={(text) => {
setText(text);
props.setData({ text: text });
}} }}
/> radius="lg"
</Tabs.Panel> defaultValue="code"
classNames={{ panel: "tabPanel" }}
>
<Tabs.List>
<Tabs.Tab value="code" leftSection={<IconCode size={12} />}>
Код
</Tabs.Tab>
<Tabs.Tab value="list" leftSection={<IconList size={12} />}>
Список
</Tabs.Tab>
</Tabs.List>
<Tabs.Panel value="list"> <Tabs.Panel value="code">
<></> <CodeTextArea
</Tabs.Panel> enabled={props.simProgress == undefined}
</Tabs> text={text || ""}
</Panel> onTextChange={(text) => {
<Separator> setText(text);
<Center props.setData({ text: text }, get_qubits(text));
style={{ }}
height: "100%", />
background: theme.colors.dark[4], </Tabs.Panel>
borderRadius: "5px",
}} <Tabs.Panel value="list">
> <></>
<IconGripVertical size={12} /> </Tabs.Panel>
</Center> </Tabs>
</Separator> </Panel>
<Panel style={{ flexShrink: 0 }} minSize={200}> <Separator>
<MoleculeViewer <Center
moleculeData={props.data ? props.data.data.text : ""} style={{
selectedAtom={selectedAtom} height: "100%",
setSelectedAtom={setSelectedAtom} background: theme.colors.dark[4],
/> borderRadius: "5px",
}}
>
<IconGripVertical size={12} />
</Center>
</Separator>
<Panel style={{ flexShrink: 0 }} minSize={200}>
<MoleculeViewer
moleculeData={props.data ? props.data.text : ""}
selectedAtom={selectedAtom}
setSelectedAtom={setSelectedAtom}
reload_counter={props.counter + counter_new}
/>
</Panel>
</Group>
</Panel> </Panel>
{props.simProgress && (
<>
<Separator>
<Center
style={{
height: "100%",
background: theme.colors.dark[4],
borderRadius: "5px",
}}
>
<IconGripHorizontal size={12} />
</Center>
</Separator>
<Panel>
{props.simProgress.error ? (
<Card withBorder style={{ height: "100%" }}>
<Group gap="xs" mb="md" orientation="vertical">
<IconAlertCircle size={20} color="#fa5252" />
<Title order={4} c="red" style={{ margin: 0 }}>
Ошибка выполнения
</Title>
<Text size="sm" c="red" fw={500} mb="xs">
{props.simProgress.error}
</Text>
</Group>
{props.simProgress.traceback && (
<>
<Text size="xs" c="dimmed" mb="xs" fw={500}>
Подробности:
</Text>
<Paper
p="xs"
withBorder
bg="dark.8"
style={{
maxHeight: "200px",
overflow: "auto",
fontFamily: "monospace",
fontSize: "11px",
}}
>
<Text
size="xs"
c="gray.5"
style={{
whiteSpace: "pre-wrap",
wordBreak: "break-all",
}}
>
{props.simProgress.traceback}
</Text>
</Paper>
</>
)}
</Card>
) : (
<Card withBorder style={{ height: "100%" }}>
<Group gap="xs">
<IconChartBar size={20} color="#4299e1" />
<Title order={4} style={{ margin: 0 }}>
Прогресс VQE
</Title>
</Group>
<SimpleGrid cols={3} mb="lg">
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={4}>
Итерация
</Text>
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={4}>
Энергия (Hartree)
</Text>
<Text size="xs" c="dimmed" tt="uppercase" fw={700} mb={4}>
Сходимость
</Text>
<Text size="xl" fw={700} c="blue">
{props.simProgress["iter_num"]}
</Text>
<Text size="xl" fw={700} c="teal">
{props.simProgress["energy"]?.toFixed(8)}
</Text>
<Text
size="xl"
fw={700}
c={
props.simProgress["conv"] < 1e-6 ? "green" : "orange"
}
>
{props.simProgress["conv"]}
</Text>
</SimpleGrid>
<details style={{ marginTop: "16px" }}>
<summary
style={{
cursor: "pointer",
fontSize: "13px",
color: "#868e96",
marginBottom: "8px",
}}
>
Показать итоговые веса (
{props.simProgress["params"]?.length})
</summary>
<Paper
p="sm"
withBorder
bg="dark.8"
style={{ maxHeight: "150px", overflow: "auto" }}
>
<Text size="xs" fw={500} mb="xs" c="dimmed">
Параметры оптимизации:
</Text>
<div
style={{
display: "flex",
flexWrap: "wrap",
gap: "8px",
}}
>
{props.simProgress["params"]?.map((param, idx) => (
<Badge
key={idx}
size="sm"
variant="outline"
color="blue"
>
p{idx}: {param.toFixed(6)}
</Badge>
))}
</div>
</Paper>
</details>
</Card>
)}
</Panel>
</>
)}
</Group> </Group>
</div> </div>
</> </>

View File

@@ -1,6 +1,7 @@
import Viewer from "miew-react"; import Viewer from "miew-react";
import Miew from "miew"; import Miew from "miew";
import { import {
LoadingOverlay,
Paper, Paper,
UnstyledButton, UnstyledButton,
useMantineColorScheme, useMantineColorScheme,
@@ -45,6 +46,7 @@ interface MoleculeViewerProps {
moleculeData: string; moleculeData: string;
selectedAtom: SelectedAtom | null; selectedAtom: SelectedAtom | null;
setSelectedAtom: (selectedAtom: SelectedAtom | null) => void; setSelectedAtom: (selectedAtom: SelectedAtom | null) => void;
reload_counter: number;
} }
function MoleculeViewer(props: MoleculeViewerProps) { function MoleculeViewer(props: MoleculeViewerProps) {
@@ -57,10 +59,16 @@ function MoleculeViewer(props: MoleculeViewerProps) {
const [isResizing, setIsResizing] = useState<boolean>(false); const [isResizing, setIsResizing] = useState<boolean>(false);
const [viewingData, setViewingData] = useState<string>(props.moleculeData); const [viewingData, setViewingData] = useState<string>(props.moleculeData);
const [isError, setIsError] = useState(false);
useEffect(() => {
setViewingData(props.moleculeData);
}, [props.reload_counter]);
//при загрузке сохраняем объект miew //при загрузке сохраняем объект miew
const onInitMiew = (miew: Miew) => { const onInitMiew = (miew: Miew) => {
setMiew(miew); setMiew(miew);
setIsError(false);
if (miew && viewingData) { if (miew && viewingData) {
//прогружаем молекулу //прогружаем молекулу
miew miew
@@ -77,15 +85,7 @@ function MoleculeViewer(props: MoleculeViewerProps) {
if (error.message == "Operation cancelled") { if (error.message == "Operation cancelled") {
return; return;
} }
notifications.show({ setIsError(true);
color: "orange",
radius: "md",
title: "Ошибка при отображении молекулы",
message:
"Проверте правильность написания кода молекулы, в нем содержатся ошибки",
icon: <IconAlertHexagon />,
style: { paddingLeft: "5px" },
});
}); });
} }
miew.setOptions({ miew.setOptions({
@@ -212,10 +212,16 @@ function MoleculeViewer(props: MoleculeViewerProps) {
backgroundColor: theme.colors.secondaryDark[7], backgroundColor: theme.colors.secondaryDark[7],
borderRadius: "5px", borderRadius: "5px",
overflow: "hidden", overflow: "hidden",
position: "relative",
}} }}
onMouseEnter={handleMouseEnter} onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave} onMouseLeave={handleMouseLeave}
> >
<LoadingOverlay
visible={isError}
loaderProps={{ children: "Ошибка отображения молекулы" }}
zIndex={8}
/>
{MemoViewer} {MemoViewer}
{isResizing && viewingData && ( {isResizing && viewingData && (
<div <div
@@ -254,7 +260,7 @@ function MoleculeViewer(props: MoleculeViewerProps) {
left: "10px", left: "10px",
top: "10px", top: "10px",
display: "flex", display: "flex",
zIndex: 5, zIndex: 9,
width: "auto", width: "auto",
}} }}
> >

View File

@@ -0,0 +1,3 @@
.StepperRoot {
flex-direction: row-reverse !important;
}

View File

@@ -0,0 +1,119 @@
import { Button, Modal, Space, Title, Select, Textarea } from "@mantine/core";
import { useEffect, useState } from "react";
import "./NewMolecule.css";
import {
ConvertMoleculeToStandart,
GetInFormats,
} from "../ConvertBackendCalls";
import { MoleculeData } from "../../Types/plugin";
interface NewMoleculeModalProps {
isOpened: boolean;
setIsOpened: (opened: boolean) => void;
setMolecule: (mol: MoleculeData) => void;
}
export function NewMoleculeModal(props: NewMoleculeModalProps) {
const [inMolecule, setInMolecule] = useState("");
const [inFormat, setInFormat] = useState<string | null>();
const [inOptions, setInOptions] = useState<string[] | undefined>();
const getOptions = () => {
GetInFormats()
.then((value: { [key: string]: string }) => {
const values = Object.keys(value);
setInOptions(values);
})
.catch(() => {});
};
useEffect(() => {
getOptions();
}, []);
//reset on open dialog
useEffect(() => {
if (props.isOpened) {
setInMolecule("");
setInFormat(undefined);
if (inOptions == undefined) {
getOptions();
}
}
}, [props.isOpened]);
const handleClose = () => {
props.setIsOpened(false);
};
const handleConvert = () => {
if (inFormat) {
ConvertMoleculeToStandart({
inputText: inMolecule,
inputFormat: inFormat,
make_3d: true,
add_h: false,
optimize: false,
}).then((value: string) => {
props.setMolecule({ text: value });
props.setIsOpened(false);
});
} else {
//TODO: add no format selected handling
}
};
const handleEmptyMolecule = () => {
props.setIsOpened(false);
};
return (
<Modal
opened={props.isOpened}
onClose={handleClose}
title=<Title size="xl">Новая молекула</Title>
centered
size="75%"
styles={{
content: { paddingLeft: "10px" },
title: { width: "100%" },
}}
>
<Textarea
className="moleculeInput"
value={inMolecule}
onChange={(e) => setInMolecule(e.currentTarget.value)}
placeholder="Введите текст молекулы"
classNames={{
wrapper: "moleculeInputWrapper",
}}
onKeyDown={(e) => {
// Stop arrow keys from reaching react-resizable-panels
if (
["ArrowUp", "ArrowDown", "ArrowLeft", "ArrowRight"].includes(e.key)
) {
e.stopPropagation();
}
}}
/>
<Space h="md" />
<Select
value={inFormat}
onChange={(value: string | null) => setInFormat(value)}
searchable
data={inOptions}
placeholder="Выберите формат"
classNames={{
input: "selectInFormat",
dropdown: "selectDropDown",
option: "selectDropDownOption",
}}
/>
<Space h="md" />
<Button color="accent" onClick={handleConvert}>
Преобразовать
</Button>
</Modal>
);
}

View File

@@ -0,0 +1,7 @@
.truncatedString {
-webkit-line-clamp: 2;
display: -webkit-box;
-webkit-box-orient: vertical;
text-overflow: ellipsis;
overflow: hidden;
}

View File

@@ -1,22 +1,158 @@
// ListItem/ListItem.tsx // ListItem/ListItem.tsx
import React from "react"; import React, { useEffect, useState } from "react";
import { MoleculeData, TaskData } from "../Types/plugin"; import { MoleculeData } from "../Types/plugin";
import { Card, Text, Title } from "@mantine/core";
import "./ListItem.css";
const ListItem: React.FunctionComponent<{ const ListItem: React.FunctionComponent<{
data: TaskData<MoleculeData> | undefined; data: MoleculeData | undefined;
simProgress: any;
}> = (props) => { }> = (props) => {
var variables = {
Multiplicity: "-",
Charge: "-",
Electrons: "-",
Orbitals: "-",
};
try {
const lines = props.data?.text.trim().split("\n") || ["", "", ""];
const secondLine = lines[1]; // "Charge=0 Multiplicity=1 Electrons=10 Orbitals=9"
variables = secondLine.split(" ").reduce(
(acc, pair) => {
const [key, value] = pair.split("=");
acc[key] = parseInt(value); // or Number(value) if you want to keep as number
return acc;
},
{} as Record<string, number>,
);
} catch {
variables = {
Multiplicity: "-",
Charge: "-",
Electrons: "-",
Orbitals: "-",
};
}
const [smiles, setSmiles] = useState<string>("");
useEffect(() => {
const response = fetch("https://mol-convert.deowl.ru/smiles", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ text: props.data?.text }),
}).then((data) => {
if (!data.ok) {
throw new Error(`HTTP error! status: ${data.status}`);
}
data.json().then((data) => {
setSmiles(data.molfile);
});
});
}, []);
return ( return (
<div <Card orientation="horizontal" style={{ height: "100%" }}>
style={{ padding: "1rem", border: "1px solid #ccc", borderRadius: "4px" }} <Card.Section
> style={{
<h3 style={{ margin: "0 0 0.5rem 0" }}>{props.data?.name}</h3> minWidth: "100px",
<p style={{ margin: "0 0 0.5rem 0", color: "#666" }}> maxWidth: "200px",
{props.data?.description} justifyContent: "space-evenly",
</p> display: "flex",
<div style={{ fontSize: "0.9rem", color: "#999" }}> flexDirection: "column",
Value: {props.data?.data.text || "Not set"} flexGrow: 1,
</div> }}
</div> px="sm"
>
<Text size="md">
Заряд: {variables.Charge != undefined ? variables.Charge : "-"}
</Text>
<Text size="md">
Мультиплексивность:{" "}
{variables.Multiplicity != undefined ? variables.Multiplicity : "-"}
</Text>
</Card.Section>
<Card.Section
style={{
minWidth: "100px",
maxWidth: "200px",
justifyContent: "space-evenly",
display: "flex",
flexDirection: "column",
flexGrow: 1,
}}
px="sm"
withBorder
>
<Text size="md">
Акт. эклектроны:{" "}
{variables.Electrons != undefined ? variables.Electrons : "-"}
</Text>
<Text size="md">
Акт. орбитали:{" "}
{variables.Orbitals != undefined ? variables.Orbitals : "-"}
</Text>
</Card.Section>
<Card.Section
style={{
minWidth: "150px",
maxWidth: "250px",
justifyContent: "space-evenly",
display: "flex",
flexDirection: "column",
flexGrow: 1,
}}
px="sm"
withBorder
>
<Text size="md" className="truncatedString">
Smiles: {smiles}
</Text>
<Text size="md">
Необходимо кубит: {Number(variables.Orbitals) * 2 || "-"}
</Text>
</Card.Section>
{props.simProgress && !props.simProgress["error"] && (
<Card.Section
style={{
width: "250px",
justifyContent: "space-evenly",
display: "flex",
flexDirection: "column",
}}
px="sm"
withBorder
>
<Title size={"md"}>Прогресс выполнения:</Title>
<Text size="md" className="truncatedString">
Итерация: {props.simProgress["iter_num"]}
</Text>
<Text size="md" className="truncatedString">
Энергия: {Math.fround(props.simProgress["energy"])}
</Text>
</Card.Section>
)}
{props.simProgress && props.simProgress["error"] && (
<Card.Section
style={{
width: "250px",
justifyContent: "space-evenly",
display: "flex",
flexDirection: "column",
}}
px="sm"
withBorder
>
<Title size={"md"}>Ошибка:</Title>
<Text size="md" className="truncatedString">
{props.simProgress["error"]}
</Text>
</Card.Section>
)}
</Card>
); );
}; };

View File

@@ -1,26 +1,14 @@
// Types
export interface TaskData<TData = any> {
id: number;
name: string;
description: string;
data: TData;
}
export interface TaskEditorProps<TData = any> {
data: TaskData<TData> | undefined;
setData: (data: TData) => void;
}
export interface TaskTypePlugin<TData = any> {
type: string;
ListItem: React.ComponentType<TaskData<TData> | undefined>;
Editor: React.ComponentType<TaskEditorProps<TData>>;
}
export interface MoleculeData { export interface MoleculeData {
text: string; text: string;
} }
export interface TaskEditorProps {
data: MoleculeData | undefined;
setData: (data: MoleculeData, qubits_needed: number) => void;
counter: number;
simProgress: any;
}
export interface SelectedAtom { export interface SelectedAtom {
serial: number; serial: number;
name: string; name: string;

View File

@@ -4,19 +4,13 @@ import "./index.css";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import Wrapper from "./wrapper"; import Wrapper from "./wrapper";
// Parse URL parameters // Parse URL parameters
const urlParams = new URLSearchParams(window.location.search);
const mode = urlParams.get("mode") == "List" ? "List" : "Editor";
// Render directly // Render directly
const rootElement = document.getElementById("root"); const rootElement = document.getElementById("root");
if (rootElement) { if (rootElement) {
rootElement.style = "height: 100%";
const root = ReactDOM.createRoot(rootElement); const root = ReactDOM.createRoot(rootElement);
root.render(<Wrapper mode={mode} />); root.render(<Wrapper />);
}
// Notify parent that plugin is ready
if (window.parent !== window) {
window.parent.postMessage({ type: "plugin-ready" }, "*");
} }

View File

@@ -11,18 +11,15 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import "./index.css"; import "./index.css";
import "@mantine/core/styles.css"; import "@mantine/core/styles.css";
import { MoleculeData, TaskData } from "./Types/plugin"; import { MoleculeData } from "./Types/plugin";
interface WrapperProps {
mode: "List" | "Editor";
}
// Create setData function that communicates with parent // Create setData function that communicates with parent
const setData = (newData: MoleculeData) => { const setData = (newData: string, qubits_needed: number) => {
window.parent.postMessage( window.parent.postMessage(
{ {
type: "plugin-update", type: "plugin-update",
data: newData, data: newData,
qubits_needed: qubits_needed,
}, },
"*", "*",
); );
@@ -69,41 +66,80 @@ const theme = createTheme({
}, },
}); });
const Wrapper: React.FunctionComponent<WrapperProps> = (props) => { const Wrapper: React.FunctionComponent = () => {
const [currentTaskData, setTaskData] = useState<TaskData<MoleculeData>>(); const [currentTaskData, setTaskData] = useState<string>();
const [currentSimProgress, setCurrentSimProgress] = useState<string>();
const [currentTheme, setTheme] = useState<"light" | "dark">("dark"); const [currentTheme, setTheme] = useState<"light" | "dark">("dark");
const [mode, setMode] = useState("List");
const [counter, setCounter] = useState(0);
// Handle messages from parent // Handle messages from parent
useEffect(() => { useEffect(() => {
window.addEventListener("message", (event) => { window.addEventListener("message", (event) => {
const { type, data } = event.data; const { type, data } = event.data;
console.log(event.data); if (type == "plugin-data") {
setTaskData(data.taskData);
setTaskData(data.taskData); setTheme(data.theme);
setTheme(data.theme); setMode(data.mode);
try {
JSON.parse(data.simProgress);
setCurrentSimProgress(data.simProgress);
} catch {
setCurrentSimProgress(undefined);
}
}
if (type == "update") {
console.log("Updating");
setCounter((prevData) => {
return prevData + 1;
});
}
}); });
}, [props.mode]);
if (props.mode === "List") { const sendReadyWithRetry = () => {
return ( window.parent.postMessage({ type: "plugin-ready" }, "*");
<MantineProvider };
colorSchemeManager={colorSchemeManager}
theme={theme} // Also send ready after a short delay to catch any late listeners
forceColorScheme={currentTheme} const timeoutId = setTimeout(sendReadyWithRetry, 100);
> }, []);
<ListItem data={currentTaskData} />
</MantineProvider> return mode === "List" ? (
); <MantineProvider
} else { colorSchemeManager={colorSchemeManager}
return ( theme={theme}
<MantineProvider forceColorScheme={currentTheme}
colorSchemeManager={colorSchemeManager} >
theme={theme} {currentTaskData && (
forceColorScheme={currentTheme} <ListItem
> data={JSON.parse(currentTaskData) as MoleculeData}
<MoleculeEditorPage data={currentTaskData} setData={setData} /> simProgress={
</MantineProvider> currentSimProgress ? JSON.parse(currentSimProgress) : undefined
); }
} />
)}
</MantineProvider>
) : (
<MantineProvider
colorSchemeManager={colorSchemeManager}
theme={theme}
forceColorScheme={currentTheme}
>
{currentTaskData && (
<MoleculeEditorPage
data={JSON.parse(currentTaskData) as MoleculeData}
setData={(data, qubits_needed) =>
setData(JSON.stringify(data), qubits_needed)
}
counter={counter}
simProgress={
currentSimProgress ? JSON.parse(currentSimProgress) : undefined
}
/>
)}
</MantineProvider>
);
}; };
export default Wrapper; export default Wrapper;

173
local_quantum/vqe.py Normal file
View File

@@ -0,0 +1,173 @@
import os
from multiprocessing.connection import Connection
from typing import List
import jax
import pennylane as qml
import pennylane.numpy as np
from jax import numpy as jnp
from pennylane import qchem
from pennylane.devices import Device
from pennylane.optimize import GradientDescentOptimizer
jax.config.update("jax_enable_x64", True)
os.environ["OMP_NUM_THREADS"] = "16"
def parse_xyz_from_text(text: str):
"""Parse XYZ format from text content."""
lines = text.strip().split("\n")
# First line: number of atoms
num_atoms = int(lines[0].strip())
# Second line: Charge/Multiplicity/Electrons/Orbitals (optional)
# Skip or parse as needed
symbols = []
coordinates = []
# Parse atom lines (after the second line)
for line in lines[2 : 2 + num_atoms]:
parts = line.strip().split()
if len(parts) >= 4:
symbol = parts[0]
x, y, z = float(parts[1]), float(parts[2]), float(parts[3])
symbols.append(symbol)
coordinates.append([x, y, z])
return symbols, coordinates
def extract_electron_info(text: str):
"""Extract electron and orbital counts from the second line."""
lines = text.strip().split("\n")
if len(lines) >= 2:
second_line = lines[1]
# Parse "Charge=0 Multiplicity=1 Electrons=3 Orbitals=3"
electrons = 3 # default
orbitals = 3 # default
charge = 0 # default
multiplicity = 1 # default
for part in second_line.split():
if "Electrons=" in part:
electrons = int(part.split("=")[1])
elif "Orbitals=" in part:
orbitals = int(part.split("=")[1])
elif "Charge=" in part:
charge = int(part.split("=")[1])
elif "Multiplicity=" in part:
multiplicity = int(part.split("=")[1])
return electrons, orbitals, charge, multiplicity
return 3, 3, 0, 1 # fallback defaults
def prepare_data(data):
text_content = data.get("text", "")
# Parse the molecular data (assuming it's in XYZ format)
symbols, coordinates = parse_xyz_from_text(text_content)
# Extract electron/orbital info (from the Charge/Multiplicity line)
# "Charge=0 Multiplicity=1 Electrons=3 Orbitals=3"
electrons, orbitals, charge, multiplicity = extract_electron_info(text_content)
return {
"symbols": symbols,
"coordinates": coordinates,
"charge": charge,
"multiplicity": multiplicity,
"active_electrons": electrons,
"active_orbitals": orbitals,
"max_iterations": data.get("max_iterations", 200),
"conv_tol": data.get("conv_tol", 1e-6),
"step_size": data.get("step_size", 0.05),
}
def run_computation(conn: Connection, dev1: Device, data: dict):
coordinates = jnp.array(data.get("coordinates"))
charge = int(data.get("charge"))
multiplicity = int(data.get("multiplicity"))
molecule = qchem.Molecule(
data.get("symbols"),
coordinates,
charge=charge,
mult=multiplicity,
)
active_electrons = int(data.get("active_electrons"))
active_orbitals = int(data.get("active_orbitals"))
max_iterations = int(data.get("max_iterations", 200))
step_size = float(data.get("step_size", 0.05))
conv_tol = float(data.get("conv_tol", 1e-6))
H, qubits = qchem.molecular_hamiltonian(
molecule,
active_electrons=active_electrons,
active_orbitals=active_orbitals,
method="openfermion",
) # type: ignore
singles, doubles = qml.qchem.excitations(active_electrons, qubits)
if False:
params = np.array(last_state, requires_grad=True)
else:
params = np.array(np.zeros(len(singles) + len(doubles)), requires_grad=True)
conn.send(
{
"iter_num": 0,
"energy": None,
"conv": None,
"params": params.tolist() if hasattr(params, "tolist") else list(params),
}
)
@qml.qnode(dev1)
def circuit(param, wires):
# Map excitations to the wires the UCCSD circuit will act on
s_wires, d_wires = qml.qchem.excitations_to_wires(singles, doubles)
qml.UCCSD(
param,
wires,
s_wires=s_wires,
d_wires=d_wires,
init_state=qml.qchem.hf_state(active_electrons, qubits),
)
return qml.expval(H)
def cost_fn(param):
return circuit(param, wires=range(qubits))
opt = GradientDescentOptimizer(stepsize=step_size)
for n in range(max_iterations):
# Take step
params, prev_energy = opt.step_and_cost(cost_fn, params)
energy = cost_fn(params)
# Calculate difference between new and old energies
conv = np.abs(energy - prev_energy)
conn.send(
{
"iter_num": n,
"energy": float(energy),
"conv": float(conv),
"params": params.tolist()
if hasattr(params, "tolist")
else list(params),
}
)
if conv <= conv_tol:
break