From 96548e60ea5e75c7eccc1ef6e4bcca2caab38165 Mon Sep 17 00:00:00 2001 From: DeOwl Date: Thu, 14 May 2026 13:27:33 +0300 Subject: [PATCH] v0.1.0 Changes: - added rabbitmq hearbeat, consumer and publisher - fixed vqe to run with rabbitmq - added frontend via fasthtml - added proper .env configuration --- .gitignore | 2 + Dockerfile => Dockerfile_build/Dockerfile | 10 +- Dockerfile_build/requirements.txt | 9 + .../source/connections/keycloak.py | 86 +++ .../source/connections/local_files.py | 69 +++ .../source/connections/quantum_backend.py | 139 +++++ .../source/connections/rabbitmq.py | 97 ++++ Dockerfile_build/source/main.py | 72 +++ Dockerfile_build/source/modules/fasthtml.py | 505 ++++++++++++++++++ Dockerfile_build/source/modules/rabbitmq.py | 326 +++++++++++ Dockerfile_build/source/modules/vqe.py | 168 ++++++ docker-compose.yml | 15 +- main.py | 103 ---- requirements.txt | 2 - source/rabbitmq_test.py | 46 -- source/vqe.py | 63 --- structure.xyz | 4 - 17 files changed, 1488 insertions(+), 228 deletions(-) rename Dockerfile => Dockerfile_build/Dockerfile (51%) create mode 100644 Dockerfile_build/requirements.txt create mode 100644 Dockerfile_build/source/connections/keycloak.py create mode 100644 Dockerfile_build/source/connections/local_files.py create mode 100644 Dockerfile_build/source/connections/quantum_backend.py create mode 100644 Dockerfile_build/source/connections/rabbitmq.py create mode 100644 Dockerfile_build/source/main.py create mode 100644 Dockerfile_build/source/modules/fasthtml.py create mode 100644 Dockerfile_build/source/modules/rabbitmq.py create mode 100644 Dockerfile_build/source/modules/vqe.py delete mode 100644 main.py delete mode 100644 requirements.txt delete mode 100644 source/rabbitmq_test.py delete mode 100644 source/vqe.py delete mode 100644 structure.xyz diff --git a/.gitignore b/.gitignore index 6a68585..6dfb133 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,5 @@ +.env +localStorage # Byte-compiled / optimized / DLL files __pycache__/ *.py[codz] diff --git a/Dockerfile b/Dockerfile_build/Dockerfile similarity index 51% rename from Dockerfile rename to Dockerfile_build/Dockerfile index ceff221..d13728a 100644 --- a/Dockerfile +++ b/Dockerfile_build/Dockerfile @@ -1,7 +1,9 @@ -FROM pennylaneai/pennylane:latest-lightning-qubit +FROM pennylaneai/pennylane:v0.45.0-lightning-qubit WORKDIR /app -COPY ./source ./source -COPY ./main.py ./ COPY ./requirements.txt ./ RUN pip install --no-cache-dir -r requirements.txt -ENTRYPOINT ["python", "main.py"] + +COPY ./source ./source + + +ENTRYPOINT ["python", "./source/main.py"] diff --git a/Dockerfile_build/requirements.txt b/Dockerfile_build/requirements.txt new file mode 100644 index 0000000..fb122db --- /dev/null +++ b/Dockerfile_build/requirements.txt @@ -0,0 +1,9 @@ +aio-pika==9.5.8 +python-fasthtml==0.14.0 +pydantic==2.13.1 +psutil==7.2.2 +python-keycloak==7.1.1 +uvicorn==0.46.0 +basis-set-exchange==0.12 +openfermionpyscf==0.5 +jax==0.10.0 diff --git a/Dockerfile_build/source/connections/keycloak.py b/Dockerfile_build/source/connections/keycloak.py new file mode 100644 index 0000000..d0de248 --- /dev/null +++ b/Dockerfile_build/source/connections/keycloak.py @@ -0,0 +1,86 @@ +import os +from datetime import timedelta + +from connections.local_files import ( + delete_client_info, + delete_token, + delete_verifier, + load_token, + save_token, +) +from fastcore.xtras import datetime + +from keycloak import KeycloakOpenID + +KEYCLOAK_CONFIG = { + "server_url": os.environ["KEYCLOAK_URL"], + "realm_name": os.environ["KEYCLOAK_REALM_NAME"], + "client_id": os.environ["KEACLOAK_CLIENT_ID"], +} + +# Инициализация клиента Keycloak +keycloak_openid = KeycloakOpenID( + server_url=KEYCLOAK_CONFIG["server_url"], + realm_name=KEYCLOAK_CONFIG["realm_name"], + client_id=KEYCLOAK_CONFIG["client_id"], +) + + +def refresh_access_token() -> dict | None: + """Refresh the access token using refresh token""" + token_data = load_token() + if not token_data or not token_data.get("refresh_token"): + return None + + try: + # Use Keycloak's refresh token endpoint + refresh_token = token_data["refresh_token"] + + # Get new token pair using refresh token + new_tokens = keycloak_openid.refresh_token(refresh_token) + + # Calculate new expiration time + expires_in = new_tokens.get("expires_in", 3600) + expires_at = datetime.now() + timedelta(seconds=expires_in) + + # Update token data + updated_token_data = { + "access_token": new_tokens["access_token"], + "refresh_token": new_tokens.get( + "refresh_token", refresh_token + ), # Some providers return new refresh token + "expires_at": expires_at.isoformat(), + "id_token": new_tokens.get("id_token", token_data.get("id_token")), + "user_info": token_data.get("user_info"), # Preserve user info + } + + # Save updated tokens + save_token(updated_token_data) + + return updated_token_data + + except Exception as e: + # If refresh fails, clear all tokens and force re-login + delete_token() + delete_client_info() + delete_verifier() + return None + + +def get_valid_access_token() -> str | None: + """Get a valid access token, refreshing if necessary""" + token_data = load_token() + + if not token_data or not token_data.get("expires_at"): + return None + + # Check if token is expired or about to expire (within 60 seconds) + expires_at = datetime.fromisoformat(str(token_data.get("expires_at"))) + if expires_at <= datetime.now() + timedelta(seconds=60): + # Token is expired or about to expire, try to refresh + new_token_data = refresh_access_token() + if new_token_data: + return new_token_data["access_token"] + return None + + return token_data["access_token"] diff --git a/Dockerfile_build/source/connections/local_files.py b/Dockerfile_build/source/connections/local_files.py new file mode 100644 index 0000000..460f43c --- /dev/null +++ b/Dockerfile_build/source/connections/local_files.py @@ -0,0 +1,69 @@ +import json +import os +from pathlib import Path + +# Локальные файлы для хранения данных +TOKEN_FILE = Path(os.environ["STORAGE_PATH"] + "/token.json") +VERIFIER_FILE = Path(os.environ["STORAGE_PATH"] + "/pkce_verifier.json") +CLIENT_INFO_FILE = Path(os.environ["STORAGE_PATH"] + "/client_info.json") + + +def save_token(token_data: dict): + """Сохранить токен в файл""" + with open(TOKEN_FILE, "w") as f: + json.dump(token_data, f, indent=2) + + +def load_token() -> dict | None: + """Загрузить токен из файла""" + if not TOKEN_FILE.exists(): + return None + with open(TOKEN_FILE, "r") as f: + return json.load(f) + + +def delete_token(): + """Удалить файл с токеном""" + if TOKEN_FILE.exists(): + TOKEN_FILE.unlink() + + +def save_verifier(verifier: str): + """Сохранить PKCE верификатор в файл""" + with open(VERIFIER_FILE, "w") as f: + json.dump({"verifier": verifier}, f) + + +def load_verifier() -> str | None: + """Загрузить PKCE верификатор из файла""" + if not VERIFIER_FILE.exists(): + return None + with open(VERIFIER_FILE, "r") as f: + data = json.load(f) + return data.get("verifier") + + +def delete_verifier(): + """Удалить файл с PKCE верификатором""" + if VERIFIER_FILE.exists(): + VERIFIER_FILE.unlink() + + +def save_client_info(client_info: dict): + """Сохранить информацию о клиенте локально""" + with open(CLIENT_INFO_FILE, "w") as f: + json.dump(client_info, f, indent=2) + + +def load_client_info() -> dict | None: + """Загрузить информацию о клиенте""" + if not CLIENT_INFO_FILE.exists(): + return None + with open(CLIENT_INFO_FILE, "r") as f: + return json.load(f) + + +def delete_client_info(): + """Удалить файл с информацией о клиенте""" + if CLIENT_INFO_FILE.exists(): + CLIENT_INFO_FILE.unlink() diff --git a/Dockerfile_build/source/connections/quantum_backend.py b/Dockerfile_build/source/connections/quantum_backend.py new file mode 100644 index 0000000..4dc10d5 --- /dev/null +++ b/Dockerfile_build/source/connections/quantum_backend.py @@ -0,0 +1,139 @@ +import os +from typing import Any, Dict + +import requests + +# Global var from env +QUANTUM_BACKEND_URL = os.getenv( + "QUANTUM_BACKEND_URL", os.environ["QUNATUM_BACKEND_URL"] +) + + +def get_or_create_device_by_name( + system_name: str, max_qubits: int, access_token +) -> Dict[str, Any]: + """ + Get or create a device by name. + + Args: + system_name: Name of the system + max_qubits: Maximum number of qubits + + Returns: + Device data as dictionary + + Raises: + requests.RequestException: If the request fails + """ + url = f"{QUANTUM_BACKEND_URL}/machine/" + payload = {"system_name": system_name, "max_qubits": max_qubits} + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {access_token}", + } + + try: + response = requests.post(url, json=payload, headers=headers) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + raise Exception(f"Failed to get or create device: {e}") from e + + +def get_device_by_id(system_id: int, access_token) -> Dict[str, Any]: + """ + Get device by ID. + + Args: + system_id: ID of the system + + Returns: + Device data as dictionary + + Raises: + requests.RequestException: If the request fails + """ + url = f"{QUANTUM_BACKEND_URL}/machine/system" + params = {"system_id": system_id} + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {access_token}", + } + + try: + response = requests.get(url, params=params, headers=headers) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + raise Exception(f"Failed to get device by ID {system_id}: {e}") from e + + +def update_device_data( + system_id: int, + system_name: str, + system_description: str, + max_qubits: int, + access_token, +) -> Dict[str, Any]: + """ + Update device data. + + Args: + system_id: ID of the system + system_name: New system name + system_description: New system description + max_qubits: New maximum qubits + + Returns: + Updated device data as dictionary + + Raises: + requests.RequestException: If the request fails + """ + url = f"{QUANTUM_BACKEND_URL}/machine" + payload = { + "system_id": system_id, + "system_name": system_name, + "system_description": system_description, + "max_qubits": max_qubits, + } + + headers = { + "Content-Type": "application/json", + "Authorization": f"Bearer {access_token}", + } + + try: + response = requests.put(url, json=payload, headers=headers) + response.raise_for_status() + return response.json() + except requests.RequestException as e: + raise Exception(f"Failed to update device data for ID {system_id}: {e}") from e + + +def get_comp_system_file(experiment_type_id: int) -> bytes: + """ + Get comp_system file as a string/blob. + + Args: + experiment_type_id: ID of the experiment type + + Returns: + Python module content as string + + Raises: + requests.RequestException: If the request fails + """ + url = f"{QUANTUM_BACKEND_URL}/experiment/types/comp-system" + params = {"experiment_type_id": experiment_type_id} + + try: + response = requests.get(url, params=params) + response.raise_for_status() + return response.content + except requests.RequestException as e: + raise Exception( + f"Failed to get comp_system file for experiment_type_id {experiment_type_id}: {e}" + ) from e diff --git a/Dockerfile_build/source/connections/rabbitmq.py b/Dockerfile_build/source/connections/rabbitmq.py new file mode 100644 index 0000000..5a1133d --- /dev/null +++ b/Dockerfile_build/source/connections/rabbitmq.py @@ -0,0 +1,97 @@ +import os +from typing import Optional + +import aio_pika +from aio_pika.abc import ( + AbstractChannel, + AbstractRobustConnection, +) +from connections.keycloak import ( + get_valid_access_token, +) +from connections.local_files import ( + load_client_info, +) + + +class RabbitMQManager: + """Singleton manager for RabbitMQ connection and channels.""" + + _instance: Optional["RabbitMQManager"] = None + _connection: Optional[AbstractRobustConnection] = None + _consumer_channel: Optional[AbstractChannel] = None + _publisher_channel: Optional[AbstractChannel] = None + _heartbeat_channel: Optional[AbstractChannel] = None + + def __new__(cls): + if cls._instance is None: + cls._instance = super().__new__(cls) + return cls._instance + + async def connect(self): + """Establish the main connection if not already connected.""" + client_info = load_client_info() + if not client_info: + return None + access_token = get_valid_access_token() + if not access_token: + return None + if self._connection is None or self._connection.is_closed: + print("Creating new RabbitMQ connection...") + self._connection = await aio_pika.connect_robust( + host=os.environ["RABBITMQ_HOST"], + port=int(os.environ["RABBITMQ_PORT"]), + login=str(client_info["system_id"]), + password=access_token, + virtualhost="/", + ) + print("RabbitMQ connection established") + return self._connection + + async def get_consumer_channel(self): + """Get channel for consuming messages.""" + if self._connection: + if self._consumer_channel is None or self._consumer_channel.is_closed: + self._consumer_channel = await self._connection.channel() + await self._consumer_channel.set_qos(prefetch_count=1, global_=True) + print("Consumer channel created") + return self._consumer_channel + return None + + async def get_publisher_channel(self): + """Get channel for publishing regular messages.""" + if self._connection: + if self._publisher_channel is None or self._publisher_channel.is_closed: + self._publisher_channel = await self._connection.channel() + print("Publisher channel created") + return self._publisher_channel + return None + + async def get_heartbeat_channel(self): + """Get channel for heartbeat publishing.""" + if self._connection: + if self._heartbeat_channel is None or self._heartbeat_channel.is_closed: + self._heartbeat_channel = await self._connection.channel() + print("Heartbeat channel created") + return self._heartbeat_channel + return None + + async def close(self): + """Close all channels and the main connection gracefully.""" + print("Closing RabbitMQ channels and connection...") + + for channel in [ + self._consumer_channel, + self._publisher_channel, + self._heartbeat_channel, + ]: + if channel and not channel.is_closed: + await channel.close() + + if self._connection and not self._connection.is_closed: + await self._connection.close() + print("RabbitMQ connection closed") + + +# Create global singleton instance +rabbitmq_manager = RabbitMQManager() diff --git a/Dockerfile_build/source/main.py b/Dockerfile_build/source/main.py new file mode 100644 index 0000000..2b7eb5c --- /dev/null +++ b/Dockerfile_build/source/main.py @@ -0,0 +1,72 @@ +import asyncio +import os +from contextlib import asynccontextmanager + +import uvicorn +from connections.rabbitmq import rabbitmq_manager +from fasthtml.core import FastHTML, Mount +from modules.fasthtml import app as fasthtml_app +from modules.rabbitmq import consume_messages_topic, publish_heartbeat + + +async def start_heartbeat(): + """Start heartbeat with auto-reconnect""" + while True: + try: + await publish_heartbeat() + except Exception as e: + print(f"Heartbeat failed: {e}") + await asyncio.sleep(5) + + +async def start_consumer(): + """Start consumer with auto-reconnect""" + while True: + try: + await consume_messages_topic() + except Exception as e: + print(f"Consumer failed: {e}") + await asyncio.sleep(5) + + +async def connect_with_retry(): + """Retry RabbitMQ connection until successful""" + while True: + try: + await rabbitmq_manager.connect() + if ( + rabbitmq_manager._connection + and not rabbitmq_manager._connection.is_closed + ): + print("RabbitMQ connected") + asyncio.create_task(start_heartbeat()) + asyncio.create_task(start_consumer()) + return + else: + await asyncio.sleep(5) + except Exception as e: + print(f"RabbitMQ connection failed: {e}, retrying in 5 seconds...") + await asyncio.sleep(5) + + +@asynccontextmanager +async def lifespan(app): + print("Starting up...") + + asyncio.create_task(connect_with_retry()) + + yield + + print("Shutting down...") + for task in asyncio.all_tasks(): + if task is not asyncio.current_task(): + task.cancel() + + await rabbitmq_manager.close() + + +app = FastHTML(routes=[Mount("", fasthtml_app, name="FastHTML")]) +app.set_lifespan(lifespan) + +if __name__ == "__main__": + uvicorn.run("main:app", host="0.0.0.0", port=int(os.environ["PORT"]), reload=True) diff --git a/Dockerfile_build/source/modules/fasthtml.py b/Dockerfile_build/source/modules/fasthtml.py new file mode 100644 index 0000000..cef4d5d --- /dev/null +++ b/Dockerfile_build/source/modules/fasthtml.py @@ -0,0 +1,505 @@ +import logging +import os +from datetime import datetime, timedelta + +import psutil +from connections.keycloak import get_valid_access_token, keycloak_openid +from connections.local_files import ( + delete_client_info, + delete_token, + delete_verifier, + load_client_info, + load_token, + load_verifier, + save_client_info, + save_token, + save_verifier, +) +from connections.quantum_backend import get_device_by_id, get_or_create_device_by_name +from connections.rabbitmq import ( + rabbitmq_manager, +) +from fasthtml.common import ( + H1, + H2, + H3, + A, + Button, + Div, + FastHTML, + Form, + Input, + Meta, + P, + RedirectResponse, + Script, + Span, + Strong, + Titled, +) +from fasthtml.pico import Card, Container, picolink +from fasthtml.xtend import Style +from keycloak.pkce_utils import generate_code_challenge, generate_code_verifier + +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + + +REDIRECT_URI = f"http://localhost:{os.environ['PORT']}/callback" + +css = Style(":root {--pico-font-size:90%,--pico-font-family: Pacifico, cursive;}") +app = FastHTML(hdrs=(picolink, css)) + + +@app.route("/", methods="get") +async def main_page(): + # Проверяем наличие валидного токена + token_data = load_token() + client_info = load_client_info() + + if token_data: + expires_at = datetime.fromisoformat(token_data.get("expires_at", "2000-01-01")) + if expires_at > datetime.now(): + # Если есть токен, но нет информации о клиенте, перенаправляем на регистрацию + if not client_info: + return RedirectResponse("/register-client", status_code=303) + return RedirectResponse("/dashboard", status_code=303) + else: + delete_token() + delete_client_info() + + return Titled( + "Главная", + H1("Добро пожаловать в Квантовый Симулятор"), + P("Пожалуйста, войдите для продолжения"), + A("Войти через Keycloak", href="/login", cls="button"), + ) + + +@app.route("/login", methods="get") +async def login(): + # Генерация PKCE кода верификатора и запроса + code_verifier = generate_code_verifier() + code_challenge, code_challenge_method = generate_code_challenge( + code_verifier, method="S256" + ) + + # Сохраняем верификатор локально + save_verifier(code_verifier) + + # Формирование URL авторизации + auth_url = keycloak_openid.auth_url( + redirect_uri=REDIRECT_URI, + scope="openid profile email", + code_challenge=code_challenge, + code_challenge_method=code_challenge_method, + ) + + return RedirectResponse(auth_url, status_code=303) + + +@app.route("/callback", methods="get") +async def callback(request): + code = request.query_params.get("code") + + if not code: + return Titled( + "Ошибка", P("Код не найден"), A("Попробовать снова", href="/login") + ) + + # Загружаем верификатор + code_verifier = load_verifier() + if not code_verifier: + return Titled( + "Ошибка", + P("PKCE верификатор не найден"), + A("Попробовать снова", href="/login"), + ) + + # Очищаем верификатор сразу после использования + delete_verifier() + + try: + # Обмен кода на токен + token = keycloak_openid.token( + grant_type="authorization_code", + code=code, + redirect_uri=REDIRECT_URI, + code_verifier=code_verifier, + ) + + # Расчет времени истечения + expires_in = token.get("expires_in", 3600) + expires_at = datetime.now() + timedelta(seconds=expires_in) + + # Получение информации о пользователе + access_token = token.get("access_token") + if not access_token: + raise Exception + user_info = keycloak_openid.userinfo(access_token) + + # Сохраняем токен + token_data_to_save = { + "access_token": access_token, + "refresh_token": token.get("refresh_token"), + "expires_at": expires_at.isoformat(), + "id_token": token.get("id_token"), + "user_info": user_info, + } + + save_token(token_data_to_save) + + # Перенаправление на регистрацию клиента (проверит, зарегистрирован ли уже) + return RedirectResponse("/register-client", status_code=303) + + except Exception as e: + logger.error(f"Ошибка при обмене токена: {e}") + return Titled("Ошибка", P(str(e)), A("Попробовать снова", href="/login")) + + +@app.route("/register-client", methods="get") +async def register_client(request): + """Страница регистрации клиента в центральном микросервисе""" + token_data = load_token() + + if not token_data: + return RedirectResponse("/login", status_code=303) + + # Проверяем, не зарегистрирован ли уже + client_info = load_client_info() + if client_info: + return RedirectResponse("/dashboard", status_code=303) + + # Проверяем истечение токена + expires_at = datetime.fromisoformat(str(token_data.get("expires_at"))) + if expires_at <= datetime.now(): + delete_token() + return RedirectResponse("/login", status_code=303) + + user_info = token_data.get("user_info", {}) + username = user_info.get( + "preferred_username", user_info.get("name", "Пользователь") + ) + + return Titled( + "Регистрация клиента", + Container( + Card( + H2("Добро пожаловать в Квантовый Симулятор!"), + P(f"Здравствуйте, {username}!"), + P("Это устройство должно быть зарегистрировано в центральном сервисе."), + P( + "Пожалуйста, укажите уникальное имя и количество кубит для этого клиента:" + ), + Form( + Input( + type="text", + name="client_name", + id="client_name", + placeholder="например: рабочий-ноутбук, домашний-пк, raspberry-pi-01", + required=True, + style="width: 100%; padding: 10px; margin: 10px 0;", + ), + Input( + type="number", + name="number_qubits", + id="number_qubits", + placeholder=0, + required=True, + style="width: 100%; padding: 10px; margin: 10px 0;", + ), + Div( + Button( + "Зарегистрировать клиент", + type="submit", + cls="button", + style="background-color: #4CAF50;", + ), + style="margin-top: 10px;", + ), + hx_post="/register-client", + hx_target="#registration-result", + hx_swap="innerHTML", + style="margin-top: 20px;", + ), + Div(id="registration-result"), + ), + Div( + Button( + "Выйти", + onclick="window.location.href='/logout'", + cls="button", + style="background-color: #f44336; margin-top: 20px;", + ), + style="text-align: center;", + ), + ), + ) + + +@app.route("/register-client", methods="post") +async def register_client_post(request): + """Обработка POST запроса регистрации клиента""" + form_data = await request.form() + client_name = form_data.get("client_name") + number_qubits = form_data.get("number_qubits") + + if not number_qubits: + return Div( + P("❌ Обязательно необходимо указать колчество кубит", style="color: red;"), + ) + + if not client_name: + return Div( + P("❌ Имя клиента обязательно", style="color: red;"), + ) + + # Проверка формата имени клиента + import re + + if not re.match(r"^[a-zA-Z0-9_-]+$", client_name): + return Div( + P( + "❌ Имя клиента может содержать только буквы, цифры, дефисы и подчеркивания", + style="color: red;", + ), + Button("Попробовать снова", onclick="location.reload()", cls="button"), + ) + + token_data = load_token() + if not token_data: + return Div( + P("❌ Сессия истекла. Пожалуйста, войдите снова.", style="color: red;"), + A("Войти", href="/login", cls="button"), + ) + + access_token = token_data.get("access_token") + + # Регистрация в центральном микросервисе + try: + response = get_or_create_device_by_name( + client_name, number_qubits, access_token + ) + save_client_info( + { + "system_id": response["system_id"], + } + ) + + return Div( + P( + "✅ Клиент успешно зарегистрирован!", + style="color: green; font-weight: bold;", + ), + P(f"Имя клиента: {client_name}"), + P("Перенаправление на панель управления..."), + Meta(http_equiv="refresh", content="2;url=/dashboard"), + Script("setTimeout(() => { window.location.href = '/dashboard'; }, 2000);"), + ) + except Exception: + return Div( + P("❌ Ошибка регистрации", style="color: red;"), + Button("Попробовать снова", onclick="location.reload()", cls="button"), + P( + "Если проблема повторяется, обратитесь к администратору.", + style="font-size: 12px; margin-top: 10px;", + ), + ) + + +@app.route("/dashboard", methods="get") +async def dashboard(): + access_token = get_valid_access_token() + if rabbitmq_manager._connection and access_token: + await rabbitmq_manager._connection.update_secret( + access_token, reason="Token expired" + ) + if not access_token: + # Token refresh failed, redirect to login + return RedirectResponse("/", status_code=303) + token_data = load_token() + client_info = load_client_info() + + if not token_data or not client_info: + return RedirectResponse("/", status_code=303) + + user_info = token_data.get("user_info", {}) + username = user_info.get( + "preferred_username", user_info.get("name", "Пользователь") + ) + system_id = client_info.get("system_id", None) + + if not token_data: + return Div( + P("❌ Сессия истекла. Пожалуйста, войдите снова.", style="color: red;"), + A("Войти", href="/login", cls="button"), + ) + try: + if system_id: + device_data = get_device_by_id(system_id, access_token) + device_name = device_data["system"]["system_name"] + max_qubits = device_data["system"]["max_qubits"] + created_at = device_data["system"]["created_at"] + else: + device_name = "error" + max_qubits = "error" + created_at = "error" + except Exception as a: + device_name = "error" + max_qubits = "error" + created_at = "error" + + return Titled( + "Панель управления", + Container( + Card( + P(Strong("Имя пользователя: "), username), + P(Strong("Email: "), user_info.get("email", "Не указан")), + P(Strong("Устройство зарегестрировано: "), created_at), + Div(style="border: 1px solid black"), + P(Strong("Имя вычислительной системы: "), device_name), + P(Strong("Макс. количество кубит: "), max_qubits), + Div(Button("Изменить", style="background-color: blue;")), + Div( + id="client-status", + hx_get="/client-status", + hx_trigger="load, every 30s", + ), + ), + Div(id="memory-stats", hx_get="/memory-stats", hx_trigger="load, every 5s"), + A( + Button( + "Выйти", + style="background-color: #f44336;", + ), + href="/logout", + style="margin: 0px;color: inherit;text-decoration: inherit;", + ), + ), + ) + + +@app.route("/client-status", methods="get") +async def status(): + """Получить статус клиента из центрального микросервиса""" + token_data = load_token() + client_info = load_client_info() + + if not token_data or not client_info: + return P("Клиент не зарегистрирован", style="color: orange;") + + if rabbitmq_manager._connection: + # TODO: FIX + status = ( + rabbitmq_manager._connection.connected + ) # get_client_status(client_name, access_token) + + if status: + return Div( + P( + Strong("Статус клиента: "), + Span("✅ Активен", style="color: green;"), + ), + style="padding: 10px; border-radius: 5px; margin: 10px 0;", + ) + else: + return Div( + P( + Strong("Статус клиента: "), + Span("Не подключен", style="color: orange;"), + ), + P( + "Не удалось связаться с центральным сервисом или данному клиенту отказано в подключении" + ), + style="padding: 10px; border-radius: 5px; margin: 10px 0;", + ) + + +@app.route("/memory-stats", methods="get") +async def memory_stats(): + """Endpoint, возвращающий карточку использования памяти (для обновления через HTMX)""" + process = psutil.Process(os.getpid()) + memory_info = process.memory_info() + current_time = datetime.now() + timestamp = current_time.strftime("%Y-%m-%d %H:%M:%S") + + def format_bytes(bytes): + for unit in ["Б", "КБ", "МБ", "ГБ"]: + if bytes < 1024.0: + return f"{bytes:.1f} {unit}" + bytes /= 1024.0 + return f"{bytes:.1f} ГБ" + + mem = 0 + if os.path.isfile("/sys/fs/cgroup/memory.max"): + with open("/sys/fs/cgroup/memory.max") as limit: + try: + mem = int(limit.read()) + except Exception: + mem = 0 + # resource.setrlimit(resource.RLIMIT_AS, (mem, mem)) + + rss_memory = format_bytes(memory_info.rss) + if mem > 0: + memory_percent = (memory_info.rss / mem) * 100 + else: + memory_percent = process.memory_percent() + + bar_color = ( + "#4CAF50" + if memory_percent < 1 + else "#FFC107" + if memory_percent < 5 + else "#F44336" + ) + + return Div( + H2("Использование памяти"), + Card( + Div( + H3(rss_memory), + style="width: 100%;display:flex;flex-direction:column;align-items:center", + ), + Div( + Div( + style=f"width: {min(memory_percent, 100)}%; background-color: {bar_color}; height: 20px; border-radius: 10px;" + ), + style="width: 100%; background-color: #e0e0e0; border-radius: 10px; overflow: hidden; margin: 10px 0;", + ), + P( + f"{memory_percent:.2f}% от системной памяти", + style="text-align: center; font-size: 12px;", + ), + P( + Strong("Обновлено: "), + timestamp, + style="text-align: center; font-size: 11px; color: #666;", + ), + ), + ) + + +@app.route("/logout", methods="get") +async def logout(): + token_data = load_token() + + if token_data and token_data.get("refresh_token"): + keycloak_openid.logout(token_data["refresh_token"]) + # Очищаем локальные файлы + delete_token() + delete_client_info() + delete_verifier() + + return Titled( + "Выход выполнен", + H2("До свидания!"), + P("Вы успешно вышли из системы."), + P("Ваш клиент был отменен в центральном сервисе."), + A("Войти снова", href="/", cls="button"), + ) + + +# Опционально: endpoint для проверки работоспособности для центрального сервиса +@app.route("/health", methods="get") +async def health(): + return {"status": "healthy", "timestamp": datetime.now().isoformat()} diff --git a/Dockerfile_build/source/modules/rabbitmq.py b/Dockerfile_build/source/modules/rabbitmq.py new file mode 100644 index 0000000..1f702f2 --- /dev/null +++ b/Dockerfile_build/source/modules/rabbitmq.py @@ -0,0 +1,326 @@ +import asyncio +import json +from multiprocessing import Pipe, Process +from multiprocessing.connection import Connection +from time import sleep + +import aio_pika +import pennylane +from aio_pika.abc import AbstractIncomingMessage +from connections.keycloak import ( + get_valid_access_token, +) +from connections.local_files import load_client_info +from connections.quantum_backend import ( + get_device_by_id, +) +from connections.rabbitmq import rabbitmq_manager +from fastcore.xtras import datetime +from modules.vqe import prepare_data, run_vqe +from pennylane.devices import Device +from uvicorn.main import logger + +HEARTBEAT_EXCHANGE = "heartbeat" +HEARTBEAT_INTERVAL = 5 +EXCHANGE_NAME = "progress_report" + +# Global flag to track if VQE is busy +vqe_busy = False +vqe_busy_lock = asyncio.Lock() + + +# --- RabbitMQ Consumer Logic --- +async def process_message(message: AbstractIncomingMessage): + """Your business logic for handling a message.""" + global vqe_busy + + async with message.process(): + body = message.body.decode() + print(f"Received and processing: {body}") + + # Parse the JSON message + data = json.loads(body) + task_id = data.get("task_id") + qubits_needed = data.get("qubits_needed") + message_data = data.get("data", {}) + + loop = asyncio.get_running_loop() + + # Extract molecular information from the text + dev = pennylane.device("lightning.qubit", wires=qubits_needed) + + sleep(1) + + await publish_message(task_id, "", "IN SYSTEM") + + data = prepare_data(message_data) + + parent_conn, child_conn = Pipe() + + # Set busy flag before starting VQE + async with vqe_busy_lock: + vqe_busy = True + + try: + # Run VQE in a separate process - will exit automatically when done + vqe_process = Process( + target=run_vqe_process, + args=(child_conn, dev, data), + ) + vqe_process.start() + + # Monitor pipe in a separate thread to avoid blocking the event loop + def monitor_pipe_thread(): + while vqe_process.is_alive(): + if parent_conn.poll(): + logger.info("polling success") + result = parent_conn.recv() + + if isinstance(result, dict) and result.get("status") == "ERROR": + asyncio.run_coroutine_threadsafe( + publish_message(task_id, json.dumps(result), "ERROR"), + loop, + ) + break + + # Check for sentinel + if result is None: + asyncio.run_coroutine_threadsafe( + publish_message(task_id, "", "COMPLETE"), loop + ) + break + + # Drain to get latest result + while parent_conn.poll(): + result = parent_conn.recv() + if result is None: + asyncio.run_coroutine_threadsafe( + publish_message(task_id, "", "COMPLETE"), loop + ) + break + + if result is not None: + logger.info(f"publishing {result}") + # Run async publish in the event loop + asyncio.run_coroutine_threadsafe( + publish_message(task_id, json.dumps(result)), loop + ) + else: + import time + + time.sleep(5) + else: + if vqe_process.exitcode == 0: + asyncio.run_coroutine_threadsafe( + publish_message(task_id, "", "COMPLETE"), loop + ) + else: + if parent_conn.poll(): + logger.info("polling success") + result = parent_conn.recv() + + if ( + isinstance(result, dict) + and result.get("status") == "ERROR" + ): + print(f"ERROR ERROR ERROR ERROR ERROR ERROR") + asyncio.run_coroutine_threadsafe( + publish_message( + task_id, json.dumps(result), "ERROR" + ), + loop, + ) + + # Run the monitor in a thread + import threading + + monitor_thread = threading.Thread(target=monitor_pipe_thread, daemon=True) + monitor_thread.start() + + # Wait for the process to finish + await asyncio.to_thread(vqe_process.join) + + # Wait for monitor thread to finish + monitor_thread.join(timeout=1) + + print(f"Completed task {task_id}") + + finally: + # Clear busy flag after VQE completes (success or failure) + async with vqe_busy_lock: + vqe_busy = False + + +def run_vqe_process(conn: Connection, dev: Device, data: dict): + """Run VQE in a separate process - process exits when this function returns.""" + try: + # Run VQE + run_vqe(conn=conn, dev1=dev, data=data) + + # Send sentinel to indicate completion + conn.send(None) # Signal completion + conn.close() + + except Exception as e: + logger.info(f"Error in VQE: {e}") + import traceback + + # Send error information through the pipe BEFORE sending None + error_msg = { + "error": str(e), + "traceback": traceback.format_exc(), + "status": "ERROR", + } + conn.send(error_msg) # Send error details + conn.send(None) # Signal completion + import time + + time.sleep(5) + conn.close() + + +async def consume_messages_topic(): + """ + Subscribe to team-specific qubit queues. + Multiple systems subscribe to the SAME queue: team_{team_id}.qubits_{N} + where N is the number of qubits this system can handle (1 to max_qubits). + + This enables round-robin task distribution among systems with sufficient qubits. + """ + channel = await rabbitmq_manager.get_consumer_channel() + if not channel: + raise Exception("Failed to get consumer channel") + + client_info = load_client_info() + if not client_info: + raise Exception("Failed to load client info") + + system_id = client_info["system_id"] + access_token = get_valid_access_token() + device_data = get_device_by_id(system_id, access_token) + teams = device_data["teams"] + if not teams: + print(f"System {system_id} is not part of any team. No queues to subscribe.") + + # Subscribe to qubit-specific queues for each team + subscription_count = 0 + for team in teams: + for qubits in range(1, team["num_qubits"] + 1): + # Queue name format: team_{team_id}.qubits_{qubits} + queue_name = f"team_{team['team']['team_id']}.qubits_{qubits}" + + # Declare the queue (durable, shared among multiple consumers) + queue = await channel.declare_queue( + queue_name, + durable=True, + arguments={ + "x-max-priority": 100, # Allow priorities 0-10 + }, + ) + + # Start consuming from this queue + await queue.consume( + process_message, + arguments={"x-priority": qubits, "x-priority-max": 10}, + ) + subscription_count += 1 + + print(f"System {system_id} subscribed to queue: {queue_name}") + + print(f"System {system_id} subscribed to {subscription_count} queues") + await asyncio.Future() # Keep running + + +# --- Heartbeat Publisher Logic --- +async def publish_heartbeat(): + """Continuously publish 'alive' or 'busy' messages using the shared connection.""" + global vqe_busy + + channel = await rabbitmq_manager.get_heartbeat_channel() + if not channel: + raise Exception("Failed to get heartbeat channel") + + exchange = await channel.declare_exchange( + HEARTBEAT_EXCHANGE, type=aio_pika.ExchangeType.FANOUT, durable=True + ) + print( + f"Heartbeat publisher started, sending status every {HEARTBEAT_INTERVAL} seconds..." + ) + + while True: + try: + client_info = load_client_info() + if not client_info: + raise Exception("Failed to load client info") + + system_id = client_info["system_id"] + + # Check current busy status + async with vqe_busy_lock: + current_status = "BUSY" if vqe_busy else "ONLINE" + + # Add timestamp for better monitoring + heartbeat_message = { + "device_id": system_id, + "status": current_status, + "timestamp": datetime.now().isoformat(), + } + + message_body = json.dumps(heartbeat_message) + await exchange.publish( + aio_pika.Message( + body=message_body.encode(), + delivery_mode=aio_pika.DeliveryMode.PERSISTENT, + ), + routing_key="", + ) + print( + f"Heartbeat sent: {current_status} at {asyncio.get_event_loop().time():.2f}" + ) + await asyncio.sleep(HEARTBEAT_INTERVAL) + + except Exception as e: + print(f"Error publishing heartbeat: {e}") + await asyncio.sleep(1) + + +async def publish_message(task_id: int, message: str, status: str = "PROCESSING"): + """Publish a message to the exchange using shared connection.""" + try: + channel = await rabbitmq_manager.get_publisher_channel() + if not channel: + raise Exception("Failed to get publisher channel") + + # Declare exchange instead of queue + exchange = await channel.declare_exchange( + EXCHANGE_NAME, # Using QUEUE_NAME as exchange name + type=aio_pika.ExchangeType.DIRECT, # or TOPIC/FANOUT based on your needs + durable=True, + ) + + client_info = load_client_info() + if not client_info: + raise Exception("Failed to load client info") + + system_id = client_info["system_id"] + + # Publish directly to the exchange + await exchange.publish( + aio_pika.Message( + body=message.encode(), + headers={ + "task_id": str(task_id), + "status": status, # "PROCESSING" or "COMPLETE" + "system_id": str(system_id), + }, + content_type="application/json", + delivery_mode=aio_pika.DeliveryMode.PERSISTENT, + ), + routing_key="", # Empty routing key for direct exchange, or use task_id as routing key + ) + print(f"Message published to exchange {EXCHANGE_NAME}: {message}") + except Exception as e: + print(f"Error publishing message: {e}") + import traceback + + traceback.print_exc() diff --git a/Dockerfile_build/source/modules/vqe.py b/Dockerfile_build/source/modules/vqe.py new file mode 100644 index 0000000..8f06f92 --- /dev/null +++ b/Dockerfile_build/source/modules/vqe.py @@ -0,0 +1,168 @@ +import os +from multiprocessing.connection import Connection + +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_vqe(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) + + 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 diff --git a/docker-compose.yml b/docker-compose.yml index ae7ce29..873b899 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,10 +1,13 @@ services: local_quantum_app: - build: - context: . + env_file: .env + image: git.deowl.ru/vkrb/client:0.1.0 + container_name: quantum-client restart: unless-stopped + extra_hosts: + - "auth.localhost:host-gateway" + - "host.docker.internal:host-gateway" ports: - - 5001:5001 - environment: - MQTT_HOST: mqtt.deowl.ru - MQTT_PORT: 1883 + - ${PORT}:${PORT} + volumes: + - ./localStorage:${STORAGE_PATH} diff --git a/main.py b/main.py deleted file mode 100644 index 8c2a9af..0000000 --- a/main.py +++ /dev/null @@ -1,103 +0,0 @@ -import asyncio -import datetime -import logging -import os -from ast import Lambda -from multiprocessing import Process, Queue -from socket import timeout -from time import sleep, time - -import aio_pika -import source.vqe as vqe -from fasthtml.common import Div, P, fast_app, serve - -logger = logging.basicConfig(level=logging.INFO) -app, rt = fast_app() - - -async def rabbit_worker(): - broker_address = os.environ.get("MQTT_HOST") # Example public broker - broker_port = os.environ.get("MQTT_PORT") - - if not broker_address or not broker_port: - if not broker_address: - logging.fatal("Not Found Environment Variable: MQTT_HOST") - if not broker_port: - logging.fatal("Not Found Environment Variable: MQTT_PORT") - logging.info("Shutting down") - return - logging.info(f"starting connection: {broker_address}, {broker_port}") - connection = await aio_pika.connect( - host=broker_address, port=int(broker_port), timeout=5 - ) - logging.info("debug") - channel = await connection.channel() - queue = await channel.declare_queue("my_queue", durable=True) - - async with queue.iterator() as queue_iter: - async for message in queue_iter: - async with message.process(): - asyncio.create_task(handle_message(message.body)) - - -async def handle_message(body): - print("Processing:", body) - await asyncio.sleep(2) # simulate work - - -def handle_done(body): - logging.info(f"done, {body}") - - -@app.on_event("startup") -async def startup(): - global rabbit_task - rabbit_task = asyncio.create_task(rabbit_worker()) - rabbit_task.add_done_callback(handle_done) - - -@app.on_event("shutdown") -async def shutdown(): - rabbit_task.cancel() - - -@rt("/") -def get(): - return Div(P("Hello World!"), hx_get="/change") - - -if __name__ == "__main__": - serve() - - """ - # GLOBAL SYSTEM PARAMETERS - active_electrons = 2 - active_orbitals = 2 - max_iterations = 500 - conv_tol = 1e-04 - step_size = 0.05 - - q = Queue() - p = Process( - target=vqe.run_vqe, - args=( - q, - symbols, - coordinates, - active_electrons, - active_orbitals, - max_iterations, - conv_tol, - step_size, - ), - ) - p.start() - while p.is_alive(): - try: - print(q.get_nowait(), datetime.datetime.now()) - except Exception as e: - print("no_data_to_get", e) - sleep(0.1) - - # client.loop_stop() # Stop the background loop thread if used - # client.disconnect()""" diff --git a/requirements.txt b/requirements.txt deleted file mode 100644 index 5eaffc8..0000000 --- a/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -aio-pika==9.5.8 -python-fasthtml==0.12.41 diff --git a/source/rabbitmq_test.py b/source/rabbitmq_test.py deleted file mode 100644 index 8b47a0c..0000000 --- a/source/rabbitmq_test.py +++ /dev/null @@ -1,46 +0,0 @@ -import os -import ssl -import sys - -import pika - -context = ssl.create_default_context() -context.verify_mode = ssl.CERT_REQUIRED - -ssl_options = pika.SSLOptions(context=context, server_hostname="rabbitmq.deowl.ru") - -credential = pika.PlainCredentials("test", "test") - - -def main(): - connection = pika.BlockingConnection( - pika.ConnectionParameters( - "rabbitmq.deowl.ru", - virtual_host="/", - port=5671, - ssl_options=ssl_options, - credentials=credential, - ) - ) - channel = connection.channel() - - channel.queue_declare(queue="hello") - - def callback(ch, method, properties, body): - print(f" [x] Received {body}") - - channel.basic_consume(queue="hello", on_message_callback=callback, auto_ack=True) - - print(" [*] Waiting for messages. To exit press CTRL+C") - channel.start_consuming() - - -if __name__ == "__main__": - try: - main() - except KeyboardInterrupt: - print("Interrupted") - try: - sys.exit(0) - except SystemExit: - os._exit(0) diff --git a/source/vqe.py b/source/vqe.py deleted file mode 100644 index f25ea8e..0000000 --- a/source/vqe.py +++ /dev/null @@ -1,63 +0,0 @@ -import os -from multiprocessing import Queue - -import pennylane as qml -from pennylane import numpy as np - -os.environ["OMP_NUM_THREADS"] = "16" - - -def get_sctructure_from_xyz_path(path: str): - return qml.qchem.read_structure("methane.xyz") - - -def run_vqe( - queue_callback: Queue, - symbols, - coordinates, - active_electrons, - active_orbitals, - max_iterations, - conv_tol, - step_size, -): - molecule = qml.qchem.Molecule(symbols, coordinates, load_data=True) - H, qubits = qml.qchem.molecular_hamiltonian( - molecule, active_electrons=active_electrons, active_orbitals=active_orbitals - ) - dev = qml.device("lightning.qubit", wires=qubits) - - singles, doubles = qml.qchem.excitations(active_electrons, qubits) - params = np.array(np.zeros(len(singles) + len(doubles)), requires_grad=True) - - @qml.qnode(dev) - 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 = qml.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) - - queue_callback.put([n, energy, params]) - - if conv <= conv_tol: - break diff --git a/structure.xyz b/structure.xyz deleted file mode 100644 index 04aaf23..0000000 --- a/structure.xyz +++ /dev/null @@ -1,4 +0,0 @@ -2 -Hydrogen molecule -H 0.000 0.000 0.000 -H 0.740 0.000 0.000 \ No newline at end of file