From c5ece3ff37ccbed132ec2792af179965c7c6a1ea Mon Sep 17 00:00:00 2001 From: DeOwl Date: Mon, 25 May 2026 19:17:04 +0300 Subject: [PATCH] v1.0 -- full integration with mol-edit backend -- added readme -- added vqe local_computational --- README.md | 198 +++++++++++ backend/src/app.py | 72 +++- backend/src/request_response_models.py | 4 + frontend-plugin/index.html | 6 +- .../EditorPage/CodeTextArea/CodeTextArea.tsx | 2 + .../src/EditorPage/ConvertBackendCalls.tsx | 62 ++++ frontend-plugin/src/EditorPage/EditorPage.tsx | 321 ++++++++++++++---- .../MoleculeViewer/MoleculeViewer.tsx | 26 +- .../EditorPage/NewMolecule/NewMolecule.css | 3 + .../EditorPage/NewMolecule/NewMolecule.tsx | 119 +++++++ frontend-plugin/src/ListItem/ListItem.css | 7 + frontend-plugin/src/ListItem/ListItem.tsx | 164 ++++++++- frontend-plugin/src/Types/plugin.tsx | 26 +- frontend-plugin/src/index.tsx | 10 +- frontend-plugin/src/wrapper.tsx | 104 ++++-- local_quantum/vqe.py | 173 ++++++++++ 16 files changed, 1134 insertions(+), 163 deletions(-) create mode 100644 README.md create mode 100644 frontend-plugin/src/EditorPage/ConvertBackendCalls.tsx create mode 100644 frontend-plugin/src/EditorPage/NewMolecule/NewMolecule.css create mode 100644 frontend-plugin/src/EditorPage/NewMolecule/NewMolecule.tsx create mode 100644 frontend-plugin/src/ListItem/ListItem.css create mode 100644 local_quantum/vqe.py diff --git a/README.md b/README.md new file mode 100644 index 0000000..f03ac90 --- /dev/null +++ b/README.md @@ -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 +``` diff --git a/backend/src/app.py b/backend/src/app.py index 1d337be..0202f9a 100644 --- a/backend/src/app.py +++ b/backend/src/app.py @@ -1,7 +1,7 @@ from collections.abc import AsyncIterator from contextlib import asynccontextmanager -from fastapi import FastAPI +from fastapi import FastAPI, Request from fastapi.exceptions import HTTPException from fastapi.middleware.cors import CORSMiddleware @@ -14,7 +14,13 @@ from logging_config import logger from openbabel import pybel from pyscf import gto # pyright: ignore 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 @@ -28,25 +34,47 @@ async def lifespan(_: FastAPI) -> AsyncIterator[None]: app = FastAPI(lifespan=lifespan) -origins = [ - "http://localhost", - "http://localhost:8001", -] +class ForceCORSHeadersMiddleware(BaseHTTPMiddleware): + async def dispatch(self, request: Request, call_next): + # Handle OPTIONS preflight requests + if request.method == "OPTIONS": + response = JSONResponse(status_code=200, content={}) + else: + response = await call_next(request) -app.add_middleware( - CORSMiddleware, - allow_origins=origins, - allow_credentials=True, - allow_methods=["*"], - allow_headers=["*"], -) + # Always add CORS headers + response.headers["Access-Control-Allow-Origin"] = "*" + response.headers["Access-Control-Allow-Methods"] = ( + "GET, POST, PUT, DELETE, OPTIONS" + ) + 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( "/convert", responses={ 200: {"description": "Coversion Successful", "model": MolFileModel}, - 400: {"description": "Item created", "model": BasicError}, + 400: {"description": "Error Converting", "model": BasicError}, }, ) def convert_molecule(req: ConvertRequest): @@ -84,6 +112,22 @@ def convert_molecule(req: ConvertRequest): 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") @cache() async def get_informats(): diff --git a/backend/src/request_response_models.py b/backend/src/request_response_models.py index c8eec84..ce09a64 100644 --- a/backend/src/request_response_models.py +++ b/backend/src/request_response_models.py @@ -9,6 +9,10 @@ class ConvertRequest(BaseModel): optimize_geometry: bool = False +class SmilesRequest(BaseModel): + text: str + + class MolFileModel(BaseModel): molfile: str diff --git a/frontend-plugin/index.html b/frontend-plugin/index.html index 360e96f..10e2d28 100644 --- a/frontend-plugin/index.html +++ b/frontend-plugin/index.html @@ -1,6 +1,6 @@ - + @@ -9,8 +9,6 @@ margin: 0; padding: 0; box-sizing: border-box; - background: none; - background-color: var(--mantine-color-secondary); } body { font-family: @@ -19,7 +17,7 @@ } - +
diff --git a/frontend-plugin/src/EditorPage/CodeTextArea/CodeTextArea.tsx b/frontend-plugin/src/EditorPage/CodeTextArea/CodeTextArea.tsx index 7688d2d..effe891 100644 --- a/frontend-plugin/src/EditorPage/CodeTextArea/CodeTextArea.tsx +++ b/frontend-plugin/src/EditorPage/CodeTextArea/CodeTextArea.tsx @@ -4,6 +4,7 @@ import { useEffect, useRef } from "react"; interface TextAreaProps { text: string; onTextChange: (text: string) => void; + enabled: boolean; } export function CodeTextArea(props: TextAreaProps) { @@ -52,6 +53,7 @@ export function CodeTextArea(props: TextAreaProps) { className="MoleculeEdit" value={props.text} ref={inputRef} + disabled={!props.enabled} onChange={(e) => { props.onTextChange(e.currentTarget.value); }} diff --git a/frontend-plugin/src/EditorPage/ConvertBackendCalls.tsx b/frontend-plugin/src/EditorPage/ConvertBackendCalls.tsx new file mode 100644 index 0000000..5cff2a9 --- /dev/null +++ b/frontend-plugin/src/EditorPage/ConvertBackendCalls.tsx @@ -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 { + 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; + } +} diff --git a/frontend-plugin/src/EditorPage/EditorPage.tsx b/frontend-plugin/src/EditorPage/EditorPage.tsx index b4e21fe..92b5d3c 100644 --- a/frontend-plugin/src/EditorPage/EditorPage.tsx +++ b/frontend-plugin/src/EditorPage/EditorPage.tsx @@ -1,90 +1,291 @@ import { Button, + Card, Center, Divider, Tabs, + Title, Text, - Textarea, useMantineTheme, + Paper, + Badge, + SimpleGrid, } from "@mantine/core"; import { Group, Panel, Separator } from "react-resizable-panels"; import "./MoleculePage.css"; import { - IconCheck, + IconAlertCircle, + IconChartBar, IconCode, + IconGripHorizontal, IconGripVertical, IconList, - IconX, } from "@tabler/icons-react"; import { CodeTextArea } from "./CodeTextArea/CodeTextArea"; import MoleculeViewer from "./MoleculeViewer/MoleculeViewer"; -import { MoleculeData, SelectedAtom, TaskEditorProps } from "../Types/plugin"; -import { useState } from "react"; +import { SelectedAtom, TaskEditorProps } from "../Types/plugin"; +import { useEffect, useState } from "react"; +import { NewMoleculeModal } from "./NewMolecule/NewMolecule"; -function MoleculeEditorPage(props: TaskEditorProps) { - const [text, setText] = useState(props.data?.data.text); +function MoleculeEditorPage(props: TaskEditorProps) { + const [text, setText] = useState(props.data?.text); const [selectedAtom, setSelectedAtom] = useState(null); 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, + ); + return variables.Orbitals * 2; + } catch { + return 0; + } + }; return ( <> -
+
+ { + props.setData(data, get_qubits(data.text)); + setText(data.text); + setTimeout(() => set_counter_new((old) => old + 2), 100); + }} + /> + {!props.simProgress && ( + + )} - - - - - }> - Код - - }> - Список - - - - - { - setText(text); - props.setData({ text: text }); + + + + + - + radius="lg" + defaultValue="code" + classNames={{ panel: "tabPanel" }} + > + + }> + Код + + }> + Список + + - - <> - - - - -
- -
-
- - + + { + setText(text); + props.setData({ text: text }, get_qubits(text)); + }} + /> + + + + <> + + + + +
+ +
+
+ + + +
+ {props.simProgress && ( + <> + +
+ +
+
+ + {props.simProgress.error ? ( + + + + + Ошибка выполнения + + + {props.simProgress.error} + + + + {props.simProgress.traceback && ( + <> + + Подробности: + + + + {props.simProgress.traceback} + + + + )} + + ) : ( + + + + + Прогресс VQE + + + + + + Итерация + + + + Энергия (Hartree) + + + + Сходимость + + + {props.simProgress["iter_num"]} + + + {props.simProgress["energy"]?.toFixed(8)} + + + {props.simProgress["conv"]} + + + +
+ + Показать итоговые веса ( + {props.simProgress["params"]?.length}) + + + + Параметры оптимизации: + +
+ {props.simProgress["params"]?.map((param, idx) => ( + + p{idx}: {param.toFixed(6)} + + ))} +
+
+
+
+ )} +
+ + )}
diff --git a/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.tsx b/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.tsx index c777370..f82f2f0 100644 --- a/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.tsx +++ b/frontend-plugin/src/EditorPage/MoleculeViewer/MoleculeViewer.tsx @@ -1,6 +1,7 @@ import Viewer from "miew-react"; import Miew from "miew"; import { + LoadingOverlay, Paper, UnstyledButton, useMantineColorScheme, @@ -45,6 +46,7 @@ interface MoleculeViewerProps { moleculeData: string; selectedAtom: SelectedAtom | null; setSelectedAtom: (selectedAtom: SelectedAtom | null) => void; + reload_counter: number; } function MoleculeViewer(props: MoleculeViewerProps) { @@ -57,10 +59,16 @@ function MoleculeViewer(props: MoleculeViewerProps) { const [isResizing, setIsResizing] = useState(false); const [viewingData, setViewingData] = useState(props.moleculeData); + const [isError, setIsError] = useState(false); + + useEffect(() => { + setViewingData(props.moleculeData); + }, [props.reload_counter]); //при загрузке сохраняем объект miew const onInitMiew = (miew: Miew) => { setMiew(miew); + setIsError(false); if (miew && viewingData) { //прогружаем молекулу miew @@ -77,15 +85,7 @@ function MoleculeViewer(props: MoleculeViewerProps) { if (error.message == "Operation cancelled") { return; } - notifications.show({ - color: "orange", - radius: "md", - title: "Ошибка при отображении молекулы", - message: - "Проверте правильность написания кода молекулы, в нем содержатся ошибки", - icon: , - style: { paddingLeft: "5px" }, - }); + setIsError(true); }); } miew.setOptions({ @@ -212,10 +212,16 @@ function MoleculeViewer(props: MoleculeViewerProps) { backgroundColor: theme.colors.secondaryDark[7], borderRadius: "5px", overflow: "hidden", + position: "relative", }} onMouseEnter={handleMouseEnter} onMouseLeave={handleMouseLeave} > + {MemoViewer} {isResizing && viewingData && (
diff --git a/frontend-plugin/src/EditorPage/NewMolecule/NewMolecule.css b/frontend-plugin/src/EditorPage/NewMolecule/NewMolecule.css new file mode 100644 index 0000000..7383265 --- /dev/null +++ b/frontend-plugin/src/EditorPage/NewMolecule/NewMolecule.css @@ -0,0 +1,3 @@ +.StepperRoot { + flex-direction: row-reverse !important; +} diff --git a/frontend-plugin/src/EditorPage/NewMolecule/NewMolecule.tsx b/frontend-plugin/src/EditorPage/NewMolecule/NewMolecule.tsx new file mode 100644 index 0000000..e0f7b16 --- /dev/null +++ b/frontend-plugin/src/EditorPage/NewMolecule/NewMolecule.tsx @@ -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(); + + const [inOptions, setInOptions] = useState(); + + 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 ( + Новая молекула + centered + size="75%" + styles={{ + content: { paddingLeft: "10px" }, + title: { width: "100%" }, + }} + > +