В прошлой статье я рассказал о создании калькулятора химических равновесий, который реализовался в виде формы на сайте и в виде мобильного приложения. Следующей идеей для реализации был еще один формат: создать страницы на сайте, каждая из которых будет посвящена равновесию между парой химических элементов.
В идеале это должно быть не просто перечисление всех реализованных фаз в соответствующем диапазоне температур, это должно быть еще и грамотное описание, что же здесь происходит и почему.
Я проверил – в базе оказалось более 4000 двухкомпонентных систем. Написать самостоятельно комментарии к каждой из них не в моих силах. Естественно, первой и единственной идеей, пришедшей на ум, были нейросети.
Нужно было выбрать вариант, который:
· Был с минимальными финансовыми затратами;
· Не занял много времени;
· Описание системы было химически грамотным.
Я выбрал LM Studio и начал перебирать варианты нейросетей, способных работать на моем железе (RTX 3090) и выдавать приемлемый результат. Таковой оказалась gemma-4-31b.
Далее была написана программа, которая извлекала из базы результаты расчетов равновесного состава для каждой пары элементов по порядку и отправляла на вход нейросети с просьбой прокомментировать, а результат сохранялся в новую базу данных:
import json import sqlite3 import re from openai import OpenAI from Phase_calculator import * def extract_source(text): match = re.search( r'<div\s+class="formula">(.*?)</div[^>]*>', text ) if not match: return None formula = match.group(1).strip() parts = formula.split() if parts: parts.pop() return ' '.join(parts) def extract_temperatures_products(text): # Извлекаем содержимое div temp_blocks = re.findall( r'<div\s+class="temper">(.*?)</div>', text, flags=re.DOTALL ) formula_blocks = re.findall( r'<div\s+class="formula">(.*?)</div>', text, flags=re.DOTALL ) temperatures = [] products = [] # Обрабатываем температуры for temp in temp_blocks: temp = temp.strip() # удалить первую букву K temp = re.sub(r'(\d+(?:\.\d+)?)\s*K', r'\1', temp, count=1) # убрать пробелы вокруг – temp = re.sub(r'\s*–\s*', '–', temp) # удалить последний : temp = temp.rstrip(':') temperatures.append(temp) # Обрабатываем продукты for formula in formula_blocks: formula = formula.strip() # удалить первое слово → formula = re.sub(r'^→\s*', '', formula) def round_number(match): value = float(match.group(0)) nearest = round(value) if abs(value - nearest) < 0.01: return str(nearest) return match.group(0) formula = re.sub(r'\d+\.\d+|\d+', round_number, formula) products.append(formula) return temperatures, products def generate_thermodynamic_comment( html_text: str, model_name: str = "local-model", base_url: str = "http://192.168.1.163:1234/v1", api_key: str = "lm-studio"): """ Генерирует научный комментарий к результатам термодинамического расчёта и возвращает его в формате HTML (<p>...</p>). http://localhost:1234/v1 API_URL = "http://localhost:1234/v1/chat/completions" API_URL = "http://192.168.1.163:1234/v1/chat/completions" """ client = OpenAI( base_url=base_url, api_key=api_key ) prompt = f""" Ты являешься специалистом в области физической химии, химической термодинамики и материаловедения. Ниже приведён фрагмент HTML-страницы с результатами термодинамического расчёта химического взаимодействия. Твоя задача — написать оригинальный научный комментарий к представленным результатам. Требования к комментарию: 1. Комментарий должен быть написан на русском языке. 2. Объём текста должен составлять примерно 150–300 слов. 3. Текст должен быть научным, грамотным и понятным инженеру, исследователю или студенту. 4. Не повторяй исходный текст дословно. 5. Не придумывай численные результаты, которых нет в тексте. 6. Объясняй возможные химические причины наблюдаемого поведения системы. 7. Допускается обсуждение: - химического сродства элементов; - устойчивости соединений; - влияния температуры; - особенностей газовой фазы; - возможных областей применения подобных расчётов. 8. Не используй фразы вида: "данная статья", "на этой странице", "в представленном материале". 9. Не используй списки, таблицы и заголовки. 10. Верни только сам комментарий. 11. Старайся избегать шаблонных фраз и генерировать уникальный текст. 12. Для разных химических систем используй различные способы объяснения результатов. 13. Если взаимодействие между веществами кинетически маловероятно, объясни причины этого. 14. Если могут образовываться устойчивые соединения, прокомментируй их возможную роль. HTML страницы: {html_text} """ response = client.chat.completions.create( model=model_name, temperature=0.7, messages=[ { "role": "system", "content": ( "Ты эксперт по химической термодинамике, " "физической химии и материаловедению." ) }, { "role": "user", "content": prompt } ] ) text = response.choices[0].message.content.strip() # Нормализация переносов paragraphs = re.split(r'\n\s*\n', text) paragraphs = [p.strip() for p in paragraphs if p.strip()] # Если модель вернула сплошной текст if len(paragraphs) == 1: paragraphs = re.split(r'(?<=\.)\s+(?=[А-ЯA-Z])', paragraphs[0]) html_result = "\n".join( f"<p>{p}</p>" for p in paragraphs if p.strip() ) return html_result def create_neyro_db(): """ Создает базу neyro.db и таблицу results. """ con = sqlite3.connect("neyro.db") cur = con.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS results ( id_elements INTEGER PRIMARY KEY, html TEXT NOT NULL ) """) con.commit() cur.close() con.close() def save_html(element1, element2, html_text): """ Принимает два номера элементов и HTML-текст. Формирует ключ по алгоритму исходной программы и записывает строку в БД. Если запись уже существует — перезаписывает ее. """ # Проверка типов if not isinstance(element1, int) or not isinstance(element2, int): raise TypeError("Номера элементов должны быть целыми числами") # Формирование ключа точно как в исходном коде elements = [element1, element2] elements.sort() key = 0 for i in range(len(elements)): key += elements[-(i + 1)] << (i * 8) con = sqlite3.connect("neyro.db") cur = con.cursor() # INSERT или UPDATE существующей строки cur.execute(""" INSERT OR REPLACE INTO results (id_elements, html) VALUES (?, ?) """, (key, html_text)) con.commit() cur.close() con.close() return key """ основная программа """ if __name__ == "__main__": culc = Phase_calculator() nomb = 0 kolvo = len(culc.elements) for n1 in range(kolvo - 1): for n2 in range(n1 + 1, kolvo + 1): nomb += 1 r = culc.return_result([n1, n2], culc.is_gases) lines = r.splitlines() first_line = lines[0] if lines else "" other_lines = "\n".join(lines[1:]) first_line = extract_source(first_line) temperatures, products = extract_temperatures_products(other_lines) t_1 = ( f"<p>На этой странице представлены результаты термодинамического расчёта взаимодействия 10 моль " f"атомов {culc.elements[n1+1]} и 10 моль атомов {culc.elements[n2+1]} в газовой фазе, с учётом следующих допущений: </p>" f"<ul><li>Приведён равновесный состав системы в указанном интервале температур при давлении 1 атм.</li>" f"<li>Каждое вещество рассматривается как индивидуальная фаза.</li>" f"<li>Фаза каждого конкретного газа-продукта не образует газовых растворов с другими фазами.</li></ul>" ) t_2 = "" for i in range(len(temperatures)): t_2 = ( f"<div class=\"reaction\"><span>{first_line}</span>" f"<span class=\"arrow\"><span class=\"condition\">{temperatures[i]}</span></span>" f"<span>{products[i]}</span></div>" ) t_1 += t_2 t_1 += f"<p>Расчёт произведён с помощью <a href=\"https://slobodovreactor.ru/culculator/\">химического калькулятора равновесного фазового состава</a>.</p>" print(nomb) print(t_1) print() comment = generate_thermodynamic_comment(t_1) print(comment) print("-----------------------------------------------------------")
Программа работала около недели. Периодически требовалась перезагрузка компьютера из-за вылета видяхи. Но в конце концов база была создана, а генерация страниц сайта на ее основе внедрена в бэкенд. Получилось как-то так.

