From eadf4820c3f8985b01e2d6e36fe36216e88daac6 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Mon, 4 Aug 2025 16:04:38 +0800 Subject: [PATCH 01/15] reaction cord --- cord/main.py | 189 ++++++++++++++++++++++++++++++++++++++++++++ cord/pyproject.toml | 12 +++ 2 files changed, 201 insertions(+) create mode 100644 cord/main.py create mode 100644 cord/pyproject.toml diff --git a/cord/main.py b/cord/main.py new file mode 100644 index 0000000..80c90fa --- /dev/null +++ b/cord/main.py @@ -0,0 +1,189 @@ +from matplotlib import pyplot as plt +import pandas as pd +import numpy as np +import io +import streamlit as st +# import scienceplots + +# plt.style.use(['nature', 'no-latex',"cjk-sc-font"]) +plt.rcParams['font.family'] = 'sans-serif' +plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置中文字体 +plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题 + +def cubic_bezier_with_zero_derivatives(p0, p1, t_array, influence_factor): + """ + 创建三次贝塞尔曲线,确保起点和终点的导数为0 + + 参数: + p0: 起点 [x0, y0] + p1: 终点 [x1, y1] + t_array: 参数数组 (0到1) + influence_factor: 影响因子,控制控制点的位置 + + 返回: + x_array, y_array: 贝塞尔曲线上的点 + """ + x0, y0 = p0 + x1, y1 = p1 + + # 计算控制点,确保起点和终点导数为0 + # 控制点位置基于影响因子和两点间距离 + dx = x1 - x0 + + # 第一个控制点:在起点右侧,y坐标与起点相同(确保起点导数为0) + p1_control = [x0 + dx * influence_factor[0], y0] + + # 第二个控制点:在终点左侧,y坐标与终点相同(确保终点导数为0) + p2_control = [x1 - dx * influence_factor[1], y1] + + # 计算贝塞尔曲线 + x_bezier = ((1-t_array)**3 * x0 + + 3*(1-t_array)**2 * t_array * p1_control[0] + + 3*(1-t_array) * t_array**2 * p2_control[0] + + t_array**3 * x1) + + y_bezier = ((1-t_array)**3 * y0 + + 3*(1-t_array)**2 * t_array * p1_control[1] + + 3*(1-t_array) * t_array**2 * p2_control[1] + + t_array**3 * y1) + + return x_bezier, y_bezier + +# @st.cache_resource +def plot_reaction_coordinate(changed=None, _lines=None): + """ + 绘制反应坐标图 + """ + # if changed is not None: + # if _lines is None: + # raise ValueError("Lines must be provided when changing a slider.") + # lines = _lines + # i = changed//2 + # x,y = lines[i].get_data() + # p1=x[0],y[0] + # p2=x[-1],y[-1] + # print(INFLU_FACTORS[i*2+1:i*2+3]) + # line = cubic_bezier_with_zero_derivatives(p1,p2, np.linspace(0, 1, 300), INFLU_FACTORS[i*2+1:i*2+3]) + # # lines[i].set_data([],[]) + # lines[i].set_data(line[0], line[1]) + # return + + + + lines = [] + fig,ax1 = plt.subplots(figsize=(9, 6)) + + last=(-1,-1) + + maxy = data["Energy"].max() + miny = data["Energy"].min() + varyy = maxy - miny + + for i in range(data.shape[0]): + line:pd.Series = data.loc[i] + if last == (-1,-1): + last = (1, line["Energy"]) + if not pd.isna(line["Name"]): + ax1.annotate(str(line["Name"]), (1, line["Energy"]+varyy*0.03), ha='center') + else: + p1 = last[0]+2,line["Energy"] + x,y = cubic_bezier_with_zero_derivatives(last,p1, np.linspace(0, 1, 300), INFLU_FACTORS[(i*2-2):i*2]) + l = ax1.plot(x, y, "-", color="black")[0] + lines.append(l) + if not pd.isna(line["Name"]): + p = p1[0],p1[1]+varyy*0.03 + ax1.annotate(str(line["Name"]), p, ha='center') + last = p1 + + + + ax1.set_xlabel("Reaction Coordinate") + ax1.xaxis.set_ticks([]) + ax1.set_ylabel("Energy (Hartree)") + ax1.set_ylim(miny-varyy*0.1, maxy+varyy*0.1) + return fig,lines + +# 创建图形和坐标轴 + +def callback_gen(x): + def callback(): + global INFLU_FACTORS + INFLU_FACTORS[x-1] = st.session_state.get(f'slider_{x}', 0.5) + plot_reaction_coordinate(changed=x, _lines=lines) + + return callback + +def on_save(): + global out_file + # for slider in slides: + # slider.ax.set_visible(False) + plt.draw() + plt.tight_layout() + out_file = io.BytesIO() + fig.savefig(out_file, format=st.session_state.get("export_format", ".tiff")[1:], dpi=300, bbox_inches='tight') + out_file.seek(0) + return out_file.getvalue() + +@st.cache_resource +def load_data(file): + # 读取数据文件 + if file is not None: + try: + data = pd.read_excel(file) if file.name.endswith((".xlsx", ".xls")) else pd.read_csv(file) + except Exception as e: + st.error(f"Error reading file: {e}") + exit() + else: + exit() + + num_factors = data.shape[0] * 2 + INFLU_FACTORS = [0.5] * data.shape[0] * 2 # 动态创建数组 + + + return data, INFLU_FACTORS + + +out_file = io.BytesIO() +st.set_page_config( + page_title="反应坐标绘制", + page_icon=":chart_with_upwards_trend:", + layout="centered", + initial_sidebar_state="expanded" +) +st.title("反应坐标绘制") +st.write("---") +col1,col2 = st.columns([0.7,0.3],gap="medium") + +with col1: + file = st.file_uploader("上传能量文件", type=["xlsx", "xls", "csv"]) + data, INFLU_FACTORS = load_data(file) + + fig,lines = plot_reaction_coordinate() + stfig = st.pyplot(fig,False) + st.selectbox("导出文件拓展名",[".tiff",".pdf",".png",".pgf"],key="export_format") + st.download_button( + label="Download Plot", + data=on_save(), + file_name="reaction_coordinate"+st.session_state.get("export_format", ".tiff"), + # mime="image/tiff" + ) +with col2: + st.write("调整滑块以改变反应坐标图曲线形状。") + for i in range(data.shape[0]): + if i!=0: + st.slider( + f'{data.loc[i,"Name"]} 左', + 0.0, 1.0, value=INFLU_FACTORS[i*2-1], + key=f'slider_{i*2}', + on_change=callback_gen(i*2) + ) + if i!= data.shape[0] - 1: + st.slider( + f'{data.loc[i,"Name"]} 右', + 0.0, 1.0, value=INFLU_FACTORS[i*2], + key=f'slider_{i*2+1}', + on_change=callback_gen(i*2+1) + ) + +st.write("---") +st.dataframe(data) diff --git a/cord/pyproject.toml b/cord/pyproject.toml new file mode 100644 index 0000000..a63ea70 --- /dev/null +++ b/cord/pyproject.toml @@ -0,0 +1,12 @@ +[project] +name = "tmp2" +version = "0.1.0" +description = "Add your description here" +readme = "README.md" +requires-python = ">=3.10" +dependencies = [ + "matplotlib>=3.10.5", + "openpyxl>=3.1.5", + "pandas>=2.3.1", + "streamlit>=1.47.1", +] From e3eb7aeeec9d67d271fcfca30705db3fce226ca6 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Mon, 4 Aug 2025 16:10:57 +0800 Subject: [PATCH 02/15] fix Reaction cord --- .gitignore | 4 +++- cord/pyproject.toml | 3 +-- mw_tool/main.py | 10 +++++++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.gitignore b/.gitignore index 53c67e9..9b9be86 100644 --- a/.gitignore +++ b/.gitignore @@ -6,4 +6,6 @@ __pycache__ *.mp3 *.mp4 *.log -test \ No newline at end of file +test +.venv +uv.lock \ No newline at end of file diff --git a/cord/pyproject.toml b/cord/pyproject.toml index a63ea70..1ecab29 100644 --- a/cord/pyproject.toml +++ b/cord/pyproject.toml @@ -1,8 +1,7 @@ [project] -name = "tmp2" +name = "Reaction-Cord" version = "0.1.0" description = "Add your description here" -readme = "README.md" requires-python = ">=3.10" dependencies = [ "matplotlib>=3.10.5", diff --git a/mw_tool/main.py b/mw_tool/main.py index 2d2b024..319d686 100644 --- a/mw_tool/main.py +++ b/mw_tool/main.py @@ -25,14 +25,18 @@ def search_compound(query): except Exception: st.error("使用smiles精确查询失败") # 尝试通过化学式搜索 + if not (isinstance(compounds, list) and len(compounds) != 0): + # 尝试通过名称搜索 + try: + compounds = pcp.get_compounds(query, 'name', listkey_count=3) + except Exception: + st.error("使用名称查询失败") + if not (isinstance(compounds, list) and len(compounds) != 0): try: compounds = pcp.get_compounds(query, 'formula', listkey_count=3) except Exception: st.error("使用化学式精确查询失败") - if not (isinstance(compounds, list) and len(compounds) != 0): - # 尝试通过名称搜索 - compounds = pcp.get_compounds(query, 'name', listkey_count=3) if isinstance(compounds, list) and len(compounds) > 0: st.info("成功查询物质基本信息,正在获取更多数据。") From 2a81a49540c06f895e3927a1db3b5ae091a0756d Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Mon, 4 Aug 2025 16:21:40 +0800 Subject: [PATCH 03/15] Reaction fix --- cord/pyproject.toml | 11 ----------- cord/requirements.txt | 4 ++++ 2 files changed, 4 insertions(+), 11 deletions(-) delete mode 100644 cord/pyproject.toml create mode 100644 cord/requirements.txt diff --git a/cord/pyproject.toml b/cord/pyproject.toml deleted file mode 100644 index 1ecab29..0000000 --- a/cord/pyproject.toml +++ /dev/null @@ -1,11 +0,0 @@ -[project] -name = "Reaction-Cord" -version = "0.1.0" -description = "Add your description here" -requires-python = ">=3.10" -dependencies = [ - "matplotlib>=3.10.5", - "openpyxl>=3.1.5", - "pandas>=2.3.1", - "streamlit>=1.47.1", -] diff --git a/cord/requirements.txt b/cord/requirements.txt new file mode 100644 index 0000000..d7d4701 --- /dev/null +++ b/cord/requirements.txt @@ -0,0 +1,4 @@ +matplotlib>=3.10.5 +openpyxl>=3.1.5 +pandas>=2.3.1 +streamlit>=1.47.1 \ No newline at end of file From b2026d6d94ad6991696ddb7acf7f890efb8e5d64 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Mon, 4 Aug 2025 16:26:48 +0800 Subject: [PATCH 04/15] mw_tool fix --- mw_tool/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mw_tool/main.py b/mw_tool/main.py index 319d686..b8449b4 100644 --- a/mw_tool/main.py +++ b/mw_tool/main.py @@ -227,9 +227,9 @@ with col1: 'found': False } st.session_state.compound_data = compound_data - st.success("✅ 分子量计算完成!") + st.success("分子量计算完成!") else: - st.error("❌ 输入的SMILES格式无效") + st.error("输入的SMILES格式无效") st.session_state.compound_data = None else: # 原有的查询逻辑 @@ -265,9 +265,9 @@ with col1: properties_found.append("熔点") if compound_data['boiling_point_src']: properties_found.append("沸点") - st.success(f"✅ 查询成功!(找到{', '.join(properties_found)}信息)") + st.success(f"查询成功!(找到{', '.join(properties_found)}信息)") else: - st.success("✅ 查询成功!(未找到物理性质信息)") + st.success("查询成功!(未找到物理性质信息)") else: # 未查询到,检查是否为SMILES @@ -630,4 +630,4 @@ with col2: st.rerun() else: - st.info("👆 请在左侧输入要查询的化学物质") + st.info("请在左侧输入要查询的化学物质") From c92340afd39cf6d445506d913beea6d51ca1542d Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Thu, 7 Aug 2025 21:16:12 +0800 Subject: [PATCH 05/15] Reaction enhance --- cord/main.py | 65 ++++++++++++++++++++++++++++++---------------------- 1 file changed, 37 insertions(+), 28 deletions(-) diff --git a/cord/main.py b/cord/main.py index 80c90fa..2097ef7 100644 --- a/cord/main.py +++ b/cord/main.py @@ -54,27 +54,13 @@ def plot_reaction_coordinate(changed=None, _lines=None): """ 绘制反应坐标图 """ - # if changed is not None: - # if _lines is None: - # raise ValueError("Lines must be provided when changing a slider.") - # lines = _lines - # i = changed//2 - # x,y = lines[i].get_data() - # p1=x[0],y[0] - # p2=x[-1],y[-1] - # print(INFLU_FACTORS[i*2+1:i*2+3]) - # line = cubic_bezier_with_zero_derivatives(p1,p2, np.linspace(0, 1, 300), INFLU_FACTORS[i*2+1:i*2+3]) - # # lines[i].set_data([],[]) - # lines[i].set_data(line[0], line[1]) - # return - - - + lines = [] fig,ax1 = plt.subplots(figsize=(9, 6)) last=(-1,-1) + maxy = data["Energy"].max() miny = data["Energy"].min() varyy = maxy - miny @@ -84,14 +70,14 @@ def plot_reaction_coordinate(changed=None, _lines=None): if last == (-1,-1): last = (1, line["Energy"]) if not pd.isna(line["Name"]): - ax1.annotate(str(line["Name"]), (1, line["Energy"]+varyy*0.03), ha='center') + ax1.annotate(str(line["Name"]), (1, line["Energy"]+varyy*K_POS[i]), ha='center') else: p1 = last[0]+2,line["Energy"] x,y = cubic_bezier_with_zero_derivatives(last,p1, np.linspace(0, 1, 300), INFLU_FACTORS[(i*2-2):i*2]) l = ax1.plot(x, y, "-", color="black")[0] lines.append(l) if not pd.isna(line["Name"]): - p = p1[0],p1[1]+varyy*0.03 + p = p1[0],p1[1]+varyy*K_POS[i] ax1.annotate(str(line["Name"]), p, ha='center') last = p1 @@ -99,17 +85,23 @@ def plot_reaction_coordinate(changed=None, _lines=None): ax1.set_xlabel("Reaction Coordinate") ax1.xaxis.set_ticks([]) - ax1.set_ylabel("Energy (Hartree)") + ax1.set_ylabel("Energy (kcal/mol)") ax1.set_ylim(miny-varyy*0.1, maxy+varyy*0.1) return fig,lines # 创建图形和坐标轴 -def callback_gen(x): - def callback(): - global INFLU_FACTORS - INFLU_FACTORS[x-1] = st.session_state.get(f'slider_{x}', 0.5) - plot_reaction_coordinate(changed=x, _lines=lines) +def callback_gen(x,typ=0): + if typ: + def callback(): + global K_POS + K_POS[x] = st.session_state.get(f'text_slider_{x}', 0.05) + plot_reaction_coordinate(changed=x, _lines=lines) + else: + def callback(): + global INFLU_FACTORS + INFLU_FACTORS[x-1] = st.session_state.get(f'slider_{x}', 0.5) + plot_reaction_coordinate(changed=x, _lines=lines) return callback @@ -138,25 +130,32 @@ def load_data(file): num_factors = data.shape[0] * 2 INFLU_FACTORS = [0.5] * data.shape[0] * 2 # 动态创建数组 + + ene = data["Energy"].to_numpy() + K_POS = np.where(ene[1:]>ene[:1],0.03,-0.05) + K_POS = [-0.05] + K_POS.tolist() + st.info(K_POS) + data["Energy"] -= data["Energy"][0] + data["Energy"]*=627.509 - return data, INFLU_FACTORS + return data, INFLU_FACTORS,K_POS out_file = io.BytesIO() st.set_page_config( page_title="反应坐标绘制", page_icon=":chart_with_upwards_trend:", - layout="centered", + layout="wide", initial_sidebar_state="expanded" ) st.title("反应坐标绘制") st.write("---") -col1,col2 = st.columns([0.7,0.3],gap="medium") +col1,col2,col3 = st.columns([0.4,0.25,0.25],gap="medium") with col1: file = st.file_uploader("上传能量文件", type=["xlsx", "xls", "csv"]) - data, INFLU_FACTORS = load_data(file) + data, INFLU_FACTORS,K_POS = load_data(file) fig,lines = plot_reaction_coordinate() stfig = st.pyplot(fig,False) @@ -185,5 +184,15 @@ with col2: on_change=callback_gen(i*2+1) ) +with col3: + st.write("调整参数以改变文字位置。") + for i in range(data.shape[0]): + st.slider( + f'{data.loc[i,"Name"]}', + -0.1, 0.1, value=K_POS[i], + key=f'text_slider_{i}', + on_change=callback_gen(i,1) + ) + st.write("---") st.dataframe(data) From 33e2aaa79af570121e8d7bcd686082d38065cfd9 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Thu, 7 Aug 2025 21:24:04 +0800 Subject: [PATCH 06/15] Reaction enhance --- cord/main.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/cord/main.py b/cord/main.py index 2097ef7..2d04204 100644 --- a/cord/main.py +++ b/cord/main.py @@ -128,7 +128,6 @@ def load_data(file): else: exit() - num_factors = data.shape[0] * 2 INFLU_FACTORS = [0.5] * data.shape[0] * 2 # 动态创建数组 ene = data["Energy"].to_numpy() @@ -159,6 +158,8 @@ with col1: fig,lines = plot_reaction_coordinate() stfig = st.pyplot(fig,False) + st.slider("字体大小",8,20, value=12, key="font_size", + on_change=lambda: plt.rcParams.update({'font.size': st.session_state.get("font_size", 12)})) st.selectbox("导出文件拓展名",[".tiff",".pdf",".png",".pgf"],key="export_format") st.download_button( label="Download Plot", From 08ba942ae60284ffd0f355392c48f4ab3670a9b8 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Tue, 19 Aug 2025 21:45:03 +0800 Subject: [PATCH 07/15] new --- mw_tool/pubchem_tool.py | 727 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 727 insertions(+) create mode 100644 mw_tool/pubchem_tool.py diff --git a/mw_tool/pubchem_tool.py b/mw_tool/pubchem_tool.py new file mode 100644 index 0000000..f612f6f --- /dev/null +++ b/mw_tool/pubchem_tool.py @@ -0,0 +1,727 @@ +import streamlit as st +import pubchempy as pcp +import re +from typing import Optional, Dict, List, cast +from io import BytesIO +import base64 +from PIL import Image +import requests +import pandas as pd +import numpy as np + +class PubChemCompound: + + def __init__(self, compound: pcp.Compound, extra: Optional[Dict[str, Optional[List[str]]]] = None): + self.cid = compound.cid + self.name = compound.iupac_name + self.formula = compound.molecular_formula + self.smiles = compound.isomeric_smiles + self.exact_mass = float(compound.exact_mass) if compound.exact_mass else None + if extra: + self.density = extra.get("density") + self.melting_point = extra.get("melting_point") + self.boiling_point = extra.get("boiling_point") + else: + self.density = None + self.melting_point = None + self.boiling_point = None + +def get_pubchem_properties(cid:str) -> Dict[str, Optional[List[str]]]: + """从PubChem获取密度、熔点、沸点信息""" + try: + # 初始化返回数据 + properties:Dict[str, Optional[List[str]]] = { + 'density': None, + 'melting_point': None, + 'boiling_point': None + } + + # 尝试获取物理化学性质相关的记录 + try: + url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug_view/data/compound/{cid}/JSON?heading=Experimental+Properties" + data = requests.get(url, timeout=3).json() + for section in data["Record"]["Section"]: + if section["TOCHeading"] == "Chemical and Physical Properties": + for sub in section["Section"]: + if sub["TOCHeading"] == "Experimental Properties": + for prop in sub["Section"]: + prop_heading = prop["TOCHeading"] + + if prop_heading == "Density" and not properties['density']: + # 可能有多条不同温度/浓度的记录,逐条返回 + properties['density'] = [ + info["Value"]["StringWithMarkup"][0]["String"] + for info in prop["Information"] + if "Value" in info and "StringWithMarkup" in info["Value"] + ] + + elif prop_heading == "Melting Point" and not properties['melting_point']: + properties['melting_point'] = [ + info["Value"]["StringWithMarkup"][0]["String"] + for info in prop["Information"] + if "Value" in info and "StringWithMarkup" in info["Value"] + ] + + elif prop_heading == "Boiling Point" and not properties['boiling_point']: + properties['boiling_point'] = [ + info["Value"]["StringWithMarkup"][0]["String"] + for info in prop["Information"] + if "Value" in info and "StringWithMarkup" in info["Value"] + ] + + return properties + + except Exception: + return properties + + except Exception as e: + # 静默处理异常,返回空的properties字典 + return { + 'density': None, + 'melting_point': None, + 'boiling_point': None + } + + +def search_compound(query: str, search_type: str = "name"): + """ + 根据不同类型搜索化合物 + + Args: + query: 搜索词 + search_type: 搜索类型 ("name", "formula", "smiles") + + Returns: + PubChem Compound对象或None + """ + try: + if search_type == "name": + compounds = pcp.get_compounds(query, 'name') + elif search_type == "formula": + compounds = pcp.get_compounds(query, 'formula') + elif search_type == "smiles": + compounds = pcp.get_compounds(query, 'smiles') + else: + return None + + if compounds is not None and len(compounds) > 0: + return compounds[0] # 返回第一个匹配的化合物 + return None + except Exception as e: + st.error(f"搜索出错: {str(e)}") + return None + + +def get_structure_image(cid: int, width: int = 300, height: int = 300): + """ + 获取化合物的2D结构图 + + Args: + cid: PubChem CID + width: 图片宽度 + height: 图片高度 + + Returns: + url + """ + url = f"https://pubchem.ncbi.nlm.nih.gov/rest/pug/compound/cid/{cid}/PNG?record_type=2d&image_size={width}x{height}" + return url + + +def extract_density_value(density_text: str) -> Optional[float]: + """ + 从密度文本中提取数值 + + Args: + density_text: 密度描述文本 + + Returns: + 提取的密度数值或None + """ + # 使用正则表达式提取数字 + pattern = r'(\d*\.\d+|\d+\.\d*|\d+)' + matches = re.findall(pattern, density_text) + if matches: + try: + return float(matches[0]) + except ValueError: + return None + return None + + +def calculate_properties(molecular_weight: float, amount_mmol: Optional[float] = None, + mass_g: Optional[float] = None, volume_ml: Optional[float] = None, + density: Optional[float] = None): + """ + 计算用量、质量、体积之间的关系 + + Args: + molecular_weight: 分子量 (g/mol) + amount_mmol: 用量 (mmol) + mass_g: 质量 (g) + volume_ml: 体积 (mL) + density: 密度 (g/mL) + + Returns: + 计算结果字典 + """ + result = { + 'amount_mmol': amount_mmol, + 'mass_g': mass_g, + 'volume_ml': volume_ml + } + + # 如果有用量和分子量,计算质量 + if amount_mmol is not None and molecular_weight: + result['mass_g'] = amount_mmol * molecular_weight / 1000 + + # 如果有质量和分子量,计算用量 + if mass_g is not None and molecular_weight: + result['amount_mmol'] = mass_g * 1000 / molecular_weight + + # 如果有质量和密度,计算体积 + if result['mass_g'] is not None and density is not None and density > 0: + result['volume_ml'] = result['mass_g'] / density + + # 如果有体积和密度,计算质量 + if volume_ml is not None and density is not None and density > 0: + result['mass_g'] = volume_ml * density + result["amount_mmol"] = result['mass_g'] * 1000 / molecular_weight + + return result + + +def reaction_table_page(): + """反应表格页面""" + st.header("⚗️ 反应表格") + + # 初始化数据 + if 'reaction_data' not in st.session_state: + st.session_state.reaction_data = pd.DataFrame([[None,None,None,None,None,None,None,None]],columns=[ + "物质", + "分子量", + "当量", + "用量(mmol)", + "质量(g)", + "密度(g/mL)", + "体积(mL)", + "备注" + ]) + + st.write("### 反应物质表格") + st.info("💡 当量为0时,该物质不参与当量计算。修改任意数值时会自动重新计算相关参数。") + + # 使用data_editor创建可编辑表格 + edited_data = st.data_editor( + st.session_state.reaction_data, + num_rows="dynamic", + use_container_width=True, + column_config={ + "物质": st.column_config.TextColumn("物质", width="medium"), + "分子量": st.column_config.NumberColumn( + "分子量", + format="%.4f", + min_value=0.0, + step=0.0001 + ), + "当量": st.column_config.NumberColumn( + "当量", + format="%.2f", + min_value=0.0, + step=0.1, + help="当量为0时不参与当量计算" + ), + "用量(mmol)": st.column_config.NumberColumn( + "用量(mmol)", + format="%.3f", + min_value=0.0, + step=0.001 + ), + "质量(g)": st.column_config.NumberColumn( + "质量(g)", + format="%.6f", + min_value=0.0, + step=0.000001 + ), + "密度(g/mL)": st.column_config.NumberColumn( + "密度(g/mL)", + format="%.3f", + min_value=0.0, + step=0.001 + ), + "体积(mL)": st.column_config.NumberColumn( + "体积(mL)", + format="%.6f", + min_value=0.0, + step=0.000001 + ), + "备注": st.column_config.TextColumn("备注", width="medium") + }, + key="reaction_table", + on_change=recalculate_reaction_data + ) + + if st.session_state.get("reaction_table_refresh",0) == 2: + st.warning("发生多个编辑,无法计算。") + st.session_state.reaction_table_refresh = 0 + + if st.session_state.get("reaction_table_refresh",0): + st.session_state.reaction_data = edited_data + st.session_state.reaction_table_refresh = 2 + st.rerun() + # print(st.session_state.reaction_data) + # 仅当返回的是 DataFrame 时再回写;如果是变更字典则由回调处理 + # if isinstance(edited_data, pd.DataFrame): + # print("Edited DataFrame:", edited_data) + # st.session_state.reaction_data = edited_data + +def recalculate_reaction_data(): + """根据最近一次编辑的行及当量,推算其他未编辑行的用量,并更新质量/体积。""" + try: + edits = st.session_state.get("reaction_table") + df = st.session_state.get("reaction_data") + + # 基本校验 + if df is None or not isinstance(df, pd.DataFrame): + return + + # 仅当从 data_editor 拿到变更字典时才处理 + if not isinstance(edits, dict): + return + print(1,edits) + + # 处理新增/删除行(若有) + for new_row in edits.get("added_rows", []) or []: + # 对象列名对齐现有表头 + print(new_row) + if isinstance(new_row, dict): + to_add = {col: new_row.get(col, None) for col in df.columns} + df = pd.concat([df, pd.DataFrame([to_add])], ignore_index=True) + for del_idx in edits.get("deleted_rows", []) or []: + try: + df.drop(index=int(del_idx), inplace=True) + except Exception: + pass + if (edits.get("deleted_rows") or []): + df.reset_index(drop=True, inplace=True) + + + edited_rows = edits.get("edited_rows", {}) or {} + if not edited_rows: + st.session_state.reaction_data = df + print("No edited rows found, skipping recalculation.") + return + if len(edited_rows) > 1: + st.session_state.reaction_table_refresh = 1 + return + + # 将编辑内容先写回到 DataFrame,记录“最后编辑的行”作为基准行 + edited_indices = [] + edited = {} + for idx_str, changes in edited_rows.items(): + try: + i = int(idx_str) + except Exception: + # 有些情况下索引就是 int + i = idx_str + edited_indices.append(i) + for col, val in changes.items(): + if col in df.columns: + edited[col] = val + if col == "当量": + df.loc[i, col] = val + if val != 0: + example = df[(df["当量"] > 0) & (df["用量(mmol)"] > 0)] + if example.size > 0: + j=0 + tmp = example.iloc[j] + print(tmp.name) + while tmp.name == i: + j+=1 + tmp = example.iloc[j] + sing = tmp['用量(mmol)']/tmp["当量"] + edited["用量(mmol)"] = sing * edited["当量"] + + basis_idx = edited_indices[-1] # 以最后一条编辑为本次基准 + + print(1,df) + + # 数值清洗工具 + def _to_float(x): + try: + if x is None: + return None + # 处理 NaN/空串 + try: + import pandas as _pd + if _pd.isna(x): + return None + except Exception: + pass + s = str(x).strip() + if s == "": + return None + return float(s) + except Exception: + return None + + # 基准行的自洽计算(用量/质量/体积) + assert edited + brow = df.loc[basis_idx] + b_mw = _to_float(brow.get("分子量")) + b_density = _to_float(brow.get("密度(g/mL)")) + b_amount = edited.get("用量(mmol)",None) + b_mass = edited.get("质量(g)",None) + b_volume = edited.get("体积(mL)",None) + b_eq = edited.get("当量",_to_float(brow.get("当量"))) + + props = calculate_properties( + molecular_weight=b_mw if b_mw else 0, + amount_mmol=b_amount, + mass_g=b_mass, + volume_ml=b_volume, + density=b_density, + ) + + _v = props.get("amount_mmol") + if isinstance(_v, (int, float)): + df.at[basis_idx, "用量(mmol)"] = round(float(_v), 6) + _v = props.get("mass_g") + if isinstance(_v, (int, float)): + df.at[basis_idx, "质量(g)"] = round(float(_v), 6) + _v = props.get("volume_ml") + if isinstance(_v, (int, float)): + df.at[basis_idx, "体积(mL)"] = round(float(_v), 6) + + print(2,df) + + # 基准行当量为 0 或不可用,则不进行当量联动计算 + if not (b_eq and b_eq > 0): + st.session_state.reaction_data = df + return + + b_amount_final = _to_float(df.at[basis_idx, "用量(mmol)"]) + if b_amount_final is None: + st.session_state.reaction_data = df + return + + base_per_eq = b_amount_final / b_eq + + print(3,df) + + # 按当量推算其他“未编辑行”的用量,并据此计算质量/体积 + for j in range(len(df)): + if j == basis_idx: + continue + if j in edited_indices: + # 本次被用户直接修改的行不改动 + continue + eq_j = _to_float(df.at[j, "当量"]) if "当量" in df.columns else None + if not (eq_j and eq_j > 0): + continue + + amt_j = base_per_eq * eq_j + df.at[j, "用量(mmol)"] = round(amt_j, 6) + + mw_j = _to_float(df.at[j, "分子量"]) if "分子量" in df.columns else None + if mw_j: + mass_j = amt_j * mw_j / 1000.0 # mmol -> mol,再乘以 g/mol + df.at[j, "质量(g)"] = round(mass_j, 6) + + dens_j = _to_float(df.at[j, "密度(g/mL)"]) if "密度(g/mL)" in df.columns else None + if dens_j and dens_j > 0: + vol_j = mass_j / dens_j + df.at[j, "体积(mL)"] = round(vol_j, 6) + print(4,df) + # 持久化 + st.session_state.reaction_data = df + except Exception as e: + raise e + print("recalculate_reaction_data error:", e) + +def add_compound_to_reaction(compound:PubChemCompound): + """将化合物添加到反应中""" + d = { + "物质":compound.formula, + "分子量":compound.exact_mass, + "当量":None, + "用量(mmol)":None, + "质量(g)":None, + "密度(g/mL)":st.session_state.get("custom_density",None), + "体积(mL)":None, + "备注":compound.name + } + st.session_state.reaction_data = pd.concat([st.session_state.reaction_data, pd.DataFrame([d])], ignore_index=True) + st.success("化合物已添加到反应中") + +def compound_search_page(): + """化合物搜索页面""" + # 输入区域 + st.header("📝 输入查询条件") + + col1, col2 = st.columns([1, 2]) + + with col1: + # 选择搜索类型 + search_type = st.selectbox( + "选择搜索类型", + ["name", "formula", "smiles"], + format_func=lambda x: {"name": "名称", "formula": "化学式", "smiles": "SMILES"}[x] + ) + + with col2: + # 输入搜索词 + query = st.text_input( + f"输入{'名称' if search_type == 'name' else '化学式' if search_type == 'formula' else 'SMILES'}", + placeholder="例如: ethanol, C2H6O, CCO" + ) + + search_button = st.button("🔍 搜索", type="primary") + + # 主要内容区域 + if search_button and query: + with st.spinner("正在搜索..."): + _compound = search_compound(query, search_type) + + if _compound is not None: + st.info("找到匹配的化合物,正在获取详细信息...") + # 在session_state中存储化合物信息 + additional_props = get_pubchem_properties(str(_compound.cid)) + st.session_state.compound = PubChemCompound(cast(pcp.Compound, _compound), additional_props) + + else: + st.error("未找到匹配的化合物,请检查输入并重试。") + return + + # 如果session_state中有化合物信息,显示结果 + if hasattr(st.session_state, 'compound') and st.session_state.compound: + compound = st.session_state.compound + st.button("添加到反应", on_click=add_compound_to_reaction, args=(compound,)) + + # 基本信息展示 + col1, col2 = st.columns(2) + + with col1: + st.header("📊 基本信息") + + st.metric("物质名称", compound.name or "未知") + st.metric("化学式", compound.formula or "未知") + st.metric("分子量", f"{compound.exact_mass:.4f} g/mol" if compound.exact_mass else "未知") + st.markdown(f"[**访问PubChem页面**](https://pubchem.ncbi.nlm.nih.gov/compound/{compound.cid})") + # 创建信息表格 + info_data = { + "属性": ["物质名称", "化学式", "分子量 (Exact Mass)", "CID"], + "值": [ + compound.name or "未知", + compound.formula or "未知", + f"{compound.exact_mass:.4f} g/mol" if compound.exact_mass else "未知", + str(compound.cid) + ] + } + + # st.table(info_data) + + with col2: + st.header("🖼️ 2D结构图") + if hasattr(compound, 'cid') and compound.cid: + structure_img = get_structure_image(compound.cid) + if structure_img: + st.image(structure_img, caption=f"CID: {compound.cid}") + else: + st.warning("无法获取结构图") + else: + st.warning("无CID信息,无法获取结构图") + + # 扩展信息 + st.markdown("---") + + # 密度信息 + with st.expander("📏 密度信息", expanded=False): + if compound.density: + st.subheader("可用密度数据:") + + # 初始化session_state中的密度选择 + if 'selected_density_idx' not in st.session_state: + st.session_state.selected_density_idx = 0 + if 'custom_density' not in st.session_state: + st.session_state.custom_density = None + + # 显示密度选项 + density_options = compound.density + selected_idx = st.radio( + "选择要使用的密度数据:", + range(len(density_options)), + format_func=lambda x: density_options[x], + key="density_radio", + index=st.session_state.selected_density_idx + ) + + # 提取密度数值 + selected_density_text = density_options[selected_idx] + extracted_density = extract_density_value(selected_density_text) + + custom_density = st.number_input( + "密度值 (g/mL):", + value=extracted_density if extracted_density else 1.0, + # min_value=0.001, + # max_value=50.0, + step=0.001, + format="%.3f", + key="custom_density_input" + ) + st.session_state.custom_density = custom_density + st.session_state.selected_density_idx = selected_idx + else: + st.warning("未找到密度数据") + st.session_state.custom_density = None + + # 熔沸点信息 + with st.expander("🌡️ 熔沸点信息", expanded=False): + col1, col2 = st.columns(2) + + with col1: + st.subheader("熔点") + if compound.melting_point: + for mp in compound.melting_point: + st.write(f"• {mp}") + else: + st.warning("未找到熔点数据") + + with col2: + st.subheader("沸点") + if compound.boiling_point: + for bp in compound.boiling_point: + st.write(f"• {bp}") + else: + st.warning("未找到沸点数据") + + # 计算器 + st.markdown("---") + st.header("🧮 用量计算器") + + if compound.exact_mass: + # 初始化session_state中的计算器数值 + if 'calc_amount' not in st.session_state: + st.session_state.calc_amount = None + if 'calc_mass' not in st.session_state: + st.session_state.calc_mass = None + if 'calc_volume' not in st.session_state: + st.session_state.calc_volume = None + + col1, col2, col3 = st.columns(3) + + with col1: + amount_mmol = st.number_input( + "用量 (mmol)", + min_value=0.0, + value=st.session_state.calc_amount if st.session_state.calc_amount else 0.0, + step=0.1, + format="%.3f", + key="amount_input", + ) + + with col2: + mass_g = st.number_input( + "质量 (g)", + min_value=0.0, + value=st.session_state.calc_mass if st.session_state.calc_mass else 0.0, + step=0.001, + format="%.6f", + key="mass_input" + ) + + with col3: + # 只有在有密度数据时才显示体积输入 + if st.session_state.get('custom_density'): + volume_ml = st.number_input( + "体积 (mL)", + min_value=0.0, + value=st.session_state.calc_volume if st.session_state.calc_volume else 0.0, + step=0.001, + format="%.6f", + key="volume_input" + ) + else: + volume_ml = None + st.info("需要密度数据才能计算体积") + + # 检测哪个值发生了变化并重新计算 + current_values = { + 'amount': amount_mmol if amount_mmol!=st.session_state.calc_amount else None, + 'mass': mass_g if mass_g != st.session_state.calc_mass else None, + 'volume': volume_ml if volume_ml and volume_ml != st.session_state.calc_volume else None + } + + # 执行计算 + if any(current_values.values()): + density = st.session_state.get('custom_density') + results = calculate_properties( + molecular_weight=compound.exact_mass, + amount_mmol=current_values['amount'], + mass_g=current_values['mass'], + volume_ml=current_values['volume'], + density=density + ) + + # 更新session_state + st.session_state.calc_amount = results['amount_mmol'] + st.session_state.calc_mass = results['mass_g'] + st.session_state.calc_volume = results['volume_ml'] + print(f"计算结果: {results}") + st.rerun() + else: + st.warning("无分子量数据,无法进行计算") + + else: + # 显示使用说明 + st.info("👈 请在左侧输入化合物信息开始查询") + + st.markdown(""" + ### 🔍 使用说明 + + 1. **选择搜索类型**: + - 名称: 输入化合物的常用名称或IUPAC名称 + - 化学式: 输入分子式 (如 C2H6O) + - SMILES: 输入SMILES字符串 (如 CCO) + + 2. **输入查询条件**: 在输入框中输入相应的查询词 + + 3. **点击搜索**: 系统将从PubChem数据库中查询匹配的化合物 + + 4. **查看结果**: + - 基本信息包括名称、化学式、分子量和2D结构图 + - 密度和熔沸点信息可在展开区域查看 + - 计算器可帮助您计算用量、质量和体积的关系 + + ### 📝 示例查询 + - **名称**: ethanol, water, glucose + - **化学式**: C2H6O, H2O, C6H12O6 + - **SMILES**: CCO, O, C(C1C(C(C(C(O1)O)O)O)O)O + """) + + +def main(): + st.set_page_config( + page_title="PubChem化合物查询工具", + page_icon="🧪", + layout="wide" + ) + + # 侧边栏导航 + with st.sidebar: + st.title("🧪 化学工具") + page = st.radio( + "选择功能页面", + ["化合物查询", "反应表格"], + index=0 + ) + + # 根据选择显示不同页面 + if page == "化合物查询": + compound_search_page() + elif page == "反应表格": + reaction_table_page() + + +if __name__ == "__main__": + main() From 547f36a074c495a8f9bc6ac3a9a7286039885211 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Tue, 19 Aug 2025 23:29:18 +0800 Subject: [PATCH 08/15] reaction table --- mw_tool/pubchem_tool.py | 152 ++++++++++++++++++++++++---------------- 1 file changed, 92 insertions(+), 60 deletions(-) diff --git a/mw_tool/pubchem_tool.py b/mw_tool/pubchem_tool.py index f612f6f..dc005bd 100644 --- a/mw_tool/pubchem_tool.py +++ b/mw_tool/pubchem_tool.py @@ -1,3 +1,4 @@ +import molmass import streamlit as st import pubchempy as pcp import re @@ -10,13 +11,23 @@ import pandas as pd import numpy as np class PubChemCompound: - - def __init__(self, compound: pcp.Compound, extra: Optional[Dict[str, Optional[List[str]]]] = None): - self.cid = compound.cid - self.name = compound.iupac_name - self.formula = compound.molecular_formula - self.smiles = compound.isomeric_smiles - self.exact_mass = float(compound.exact_mass) if compound.exact_mass else None + def __init__(self, + compound: Optional[pcp.Compound]=None, + extra: Optional[Dict[str, Optional[List[str]]]] = None, + **kwargs + ): + if compound: + self.cid = compound.cid + self.name = compound.iupac_name + self.formula = compound.molecular_formula + self.smiles = compound.isomeric_smiles + self.exact_mass = float(compound.exact_mass) if compound.exact_mass else None + else: + self.cid = None + self.name = None + self.formula = None + self.smiles = None + self.exact_mass = None if extra: self.density = extra.get("density") self.melting_point = extra.get("melting_point") @@ -25,6 +36,7 @@ class PubChemCompound: self.density = None self.melting_point = None self.boiling_point = None + self.__dict__.update(kwargs) # 允许传入其他属性 def get_pubchem_properties(cid:str) -> Dict[str, Optional[List[str]]]: """从PubChem获取密度、熔点、沸点信息""" @@ -210,7 +222,11 @@ def reaction_table_page(): st.write("### 反应物质表格") st.info("💡 当量为0时,该物质不参与当量计算。修改任意数值时会自动重新计算相关参数。") - + use_on_change = st.selectbox("是否立即刷新", options=["是", "否"], index=0) + + def raise_NotImplementedError(info=None): + """占位函数,避免未实现的回调错误""" + raise NotImplementedError("暂未实现延迟计算") # 使用data_editor创建可编辑表格 edited_data = st.data_editor( st.session_state.reaction_data, @@ -258,11 +274,12 @@ def reaction_table_page(): "备注": st.column_config.TextColumn("备注", width="medium") }, key="reaction_table", - on_change=recalculate_reaction_data + on_change=recalculate_reaction_data if use_on_change == "是" else raise_NotImplementedError ) if st.session_state.get("reaction_table_refresh",0) == 2: st.warning("发生多个编辑,无法计算。") + st.warning("由于计算失败,当前表格内容可能存在错误。") st.session_state.reaction_table_refresh = 0 if st.session_state.get("reaction_table_refresh",0): @@ -288,12 +305,10 @@ def recalculate_reaction_data(): # 仅当从 data_editor 拿到变更字典时才处理 if not isinstance(edits, dict): return - print(1,edits) # 处理新增/删除行(若有) for new_row in edits.get("added_rows", []) or []: # 对象列名对齐现有表头 - print(new_row) if isinstance(new_row, dict): to_add = {col: new_row.get(col, None) for col in df.columns} df = pd.concat([df, pd.DataFrame([to_add])], ignore_index=True) @@ -327,15 +342,15 @@ def recalculate_reaction_data(): edited_indices.append(i) for col, val in changes.items(): if col in df.columns: + df.loc[i, col] = val edited[col] = val + # if col in ["用量(mmol)","质量(g)","体积(mL)","密度(g/mL)","当量"]: if col == "当量": - df.loc[i, col] = val if val != 0: example = df[(df["当量"] > 0) & (df["用量(mmol)"] > 0)] if example.size > 0: j=0 tmp = example.iloc[j] - print(tmp.name) while tmp.name == i: j+=1 tmp = example.iloc[j] @@ -344,7 +359,6 @@ def recalculate_reaction_data(): basis_idx = edited_indices[-1] # 以最后一条编辑为本次基准 - print(1,df) # 数值清洗工具 def _to_float(x): @@ -366,14 +380,25 @@ def recalculate_reaction_data(): return None # 基准行的自洽计算(用量/质量/体积) - assert edited + brow = df.loc[basis_idx] + if "密度(g/mL)" in edited.keys(): + df.loc[basis_idx, "密度(g/mL)"] = edited["密度(g/mL)"] + if brow.get("体积(mL)") is None and "质量(g)" in brow.keys(): + edited["质量(g)"] = _to_float(brow.get("质量(g)")) + elif brow.get("质量(g)") is None and "体积(mL)" in brow.keys(): + edited["体积(mL)"] = _to_float(brow.get("体积(mL)")) + else: + st.error("当质量和体积同时存在时,修改密度为未定义行为。") + st.warning("由于计算失败,当前表格内容可能存在错误。") + return + b_mw = _to_float(brow.get("分子量")) - b_density = _to_float(brow.get("密度(g/mL)")) - b_amount = edited.get("用量(mmol)",None) - b_mass = edited.get("质量(g)",None) - b_volume = edited.get("体积(mL)",None) - b_eq = edited.get("当量",_to_float(brow.get("当量"))) + b_density = edited.get("密度(g/mL)", _to_float(brow.get("密度(g/mL)"))) + b_amount = edited.get("用量(mmol)", None) + b_mass = edited.get("质量(g)", None) + b_volume = edited.get("体积(mL)", None) + b_eq = _to_float(brow.get("当量")) props = calculate_properties( molecular_weight=b_mw if b_mw else 0, @@ -393,7 +418,6 @@ def recalculate_reaction_data(): if isinstance(_v, (int, float)): df.at[basis_idx, "体积(mL)"] = round(float(_v), 6) - print(2,df) # 基准行当量为 0 或不可用,则不进行当量联动计算 if not (b_eq and b_eq > 0): @@ -407,7 +431,6 @@ def recalculate_reaction_data(): base_per_eq = b_amount_final / b_eq - print(3,df) # 按当量推算其他“未编辑行”的用量,并据此计算质量/体积 for j in range(len(df)): @@ -432,11 +455,11 @@ def recalculate_reaction_data(): if dens_j and dens_j > 0: vol_j = mass_j / dens_j df.at[j, "体积(mL)"] = round(vol_j, 6) - print(4,df) # 持久化 st.session_state.reaction_data = df except Exception as e: - raise e + # raise e + st.error("重新计算反应数据时出错,表格可能有误") print("recalculate_reaction_data error:", e) def add_compound_to_reaction(compound:PubChemCompound): @@ -463,16 +486,17 @@ def compound_search_page(): with col1: # 选择搜索类型 + mp = {"name": "名称", "formula": "化学式", "smiles": "SMILES","calc":"化学式(本地计算)"} search_type = st.selectbox( "选择搜索类型", - ["name", "formula", "smiles"], - format_func=lambda x: {"name": "名称", "formula": "化学式", "smiles": "SMILES"}[x] + mp.keys(), + format_func=lambda x: mp[x] ) with col2: # 输入搜索词 query = st.text_input( - f"输入{'名称' if search_type == 'name' else '化学式' if search_type == 'formula' else 'SMILES'}", + f"输入{mp[search_type]}", placeholder="例如: ethanol, C2H6O, CCO" ) @@ -480,18 +504,34 @@ def compound_search_page(): # 主要内容区域 if search_button and query: - with st.spinner("正在搜索..."): - _compound = search_compound(query, search_type) - - if _compound is not None: - st.info("找到匹配的化合物,正在获取详细信息...") - # 在session_state中存储化合物信息 - additional_props = get_pubchem_properties(str(_compound.cid)) - st.session_state.compound = PubChemCompound(cast(pcp.Compound, _compound), additional_props) - + if search_type == "calc": + try: + mass = molmass.Formula(query).mass + st.session_state.compound = PubChemCompound(exact_mass=mass, formula=query) + except Exception as e: + st.error(f"计算分子量时出错: {e}") else: - st.error("未找到匹配的化合物,请检查输入并重试。") - return + with st.spinner("正在搜索..."): + _compound = search_compound(query, search_type) + + print(_compound,search_type) + + if _compound is not None: + st.info("找到匹配的化合物,正在获取详细信息...") + # 在session_state中存储化合物信息 + additional_props = get_pubchem_properties(str(_compound.cid)) + st.session_state.compound = PubChemCompound(cast(pcp.Compound, _compound), additional_props) + + elif search_type == "formula": + try: + mass = molmass.Formula(query).mass + st.session_state.compound = PubChemCompound(exact_mass=mass, formula=query) + print(mass) + st.info("根据化学式计算得到分子量") + except Exception as e: + st.error(f"计算分子量时出错: {e}") + else: + st.error("未找到匹配的化合物,请检查输入并重试。") # 如果session_state中有化合物信息,显示结果 if hasattr(st.session_state, 'compound') and st.session_state.compound: @@ -503,21 +543,14 @@ def compound_search_page(): with col1: st.header("📊 基本信息") - + st.metric("物质名称", compound.name or "未知") st.metric("化学式", compound.formula or "未知") st.metric("分子量", f"{compound.exact_mass:.4f} g/mol" if compound.exact_mass else "未知") - st.markdown(f"[**访问PubChem页面**](https://pubchem.ncbi.nlm.nih.gov/compound/{compound.cid})") + if compound.cid: + st.markdown(f"[**访问PubChem页面**](https://pubchem.ncbi.nlm.nih.gov/compound/{compound.cid})") # 创建信息表格 - info_data = { - "属性": ["物质名称", "化学式", "分子量 (Exact Mass)", "CID"], - "值": [ - compound.name or "未知", - compound.formula or "未知", - f"{compound.exact_mass:.4f} g/mol" if compound.exact_mass else "未知", - str(compound.cid) - ] - } + # st.table(info_data) @@ -536,8 +569,8 @@ def compound_search_page(): st.markdown("---") # 密度信息 - with st.expander("📏 密度信息", expanded=False): - if compound.density: + if compound.density: + with st.expander("📏 密度信息", expanded=False): st.subheader("可用密度数据:") # 初始化session_state中的密度选择 @@ -571,16 +604,16 @@ def compound_search_page(): ) st.session_state.custom_density = custom_density st.session_state.selected_density_idx = selected_idx - else: - st.warning("未找到密度数据") - st.session_state.custom_density = None + else: + st.session_state.custom_density = None # 熔沸点信息 - with st.expander("🌡️ 熔沸点信息", expanded=False): - col1, col2 = st.columns(2) - - with col1: - st.subheader("熔点") + if compound.melting_point or compound.boiling_point: + with st.expander("🌡️ 熔沸点信息", expanded=False): + col1, col2 = st.columns(2) + + with col1: + st.subheader("熔点") if compound.melting_point: for mp in compound.melting_point: st.write(f"• {mp}") @@ -643,7 +676,6 @@ def compound_search_page(): ) else: volume_ml = None - st.info("需要密度数据才能计算体积") # 检测哪个值发生了变化并重新计算 current_values = { From 397084995e0bfa143030835c4119b281989f6b03 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Wed, 20 Aug 2025 09:39:00 +0800 Subject: [PATCH 09/15] update --- mw_tool/pubchem_tool.py | 131 +++++++++++++++++++++++++++++++++++----- 1 file changed, 116 insertions(+), 15 deletions(-) diff --git a/mw_tool/pubchem_tool.py b/mw_tool/pubchem_tool.py index dc005bd..ed647fa 100644 --- a/mw_tool/pubchem_tool.py +++ b/mw_tool/pubchem_tool.py @@ -209,7 +209,7 @@ def reaction_table_page(): # 初始化数据 if 'reaction_data' not in st.session_state: - st.session_state.reaction_data = pd.DataFrame([[None,None,None,None,None,None,None,None]],columns=[ + df = pd.DataFrame([[None,None,None,None,None,None,None,None]],columns=[ "物质", "分子量", "当量", @@ -218,15 +218,19 @@ def reaction_table_page(): "密度(g/mL)", "体积(mL)", "备注" - ]) + ],dtype="float") + df["物质"] = df["物质"].astype("string") + df["备注"] = df["备注"].astype("string") + st.session_state.reaction_data = df + st.write("### 反应物质表格") - st.info("💡 当量为0时,该物质不参与当量计算。修改任意数值时会自动重新计算相关参数。") - use_on_change = st.selectbox("是否立即刷新", options=["是", "否"], index=0) + use_on_change = st.checkbox("是否立即计算", value=True) + st.info(f"💡 当量为0时,该物质不参与当量计算。"+("如果保证表格已有内容自洽,可选中不使用检查。" if not use_on_change else "")) + if not use_on_change: + st.button("计算",on_click=calc_reaction_data) + st.checkbox("不使用检查", value=False,key="no_check",) - def raise_NotImplementedError(info=None): - """占位函数,避免未实现的回调错误""" - raise NotImplementedError("暂未实现延迟计算") # 使用data_editor创建可编辑表格 edited_data = st.data_editor( st.session_state.reaction_data, @@ -274,9 +278,12 @@ def reaction_table_page(): "备注": st.column_config.TextColumn("备注", width="medium") }, key="reaction_table", - on_change=recalculate_reaction_data if use_on_change == "是" else raise_NotImplementedError + on_change=recalculate_reaction_data if use_on_change else None ) - + + if not use_on_change and isinstance(edited_data, pd.DataFrame): + st.session_state.static_reaction_data = edited_data + if st.session_state.get("reaction_table_refresh",0) == 2: st.warning("发生多个编辑,无法计算。") st.warning("由于计算失败,当前表格内容可能存在错误。") @@ -286,12 +293,88 @@ def reaction_table_page(): st.session_state.reaction_data = edited_data st.session_state.reaction_table_refresh = 2 st.rerun() + # print(st.session_state.reaction_data) # 仅当返回的是 DataFrame 时再回写;如果是变更字典则由回调处理 # if isinstance(edited_data, pd.DataFrame): # print("Edited DataFrame:", edited_data) # st.session_state.reaction_data = edited_data +def calc_reaction_data(): + try: + df: pd.DataFrame = st.session_state.get("static_reaction_data",None) + if not isinstance(df, pd.DataFrame): + st.error("反应数据格式不正确") + raise ValueError("reaction_data must be a DataFrame") + df.columns = ["name","mw","eq","mol","mass","rho","vol","note"] + + # validate + if not st.session_state.get("no_check",False): + if any(df["mw"].notna() & df["mol"].notna() & df["mass"].notna()): + st.error("分子量、物质的量和质量不能同时存在") + raise ValueError + if any((df["mass"].notna() | (df["mw"].notna() & df["mol"].notna())) & df["vol"].notna() & df["rho"].notna()): + st.error("质量、体积和密度不能同时存在或可求") + raise ValueError + + # mol -> mass + fil = df["mw"].notna() & df["mol"].notna() + df.loc[fil, "mass"] = df[fil]["mol"] * df[f"{'mw'}"] / 1000.0 # mmol -> mol,再乘以 g/mol + + # mass -> mol + fil = df["mw"].notna() & df["mass"].notna() + df.loc[fil, "mol"] = df[fil]["mass"] * 1000.0 / df[f"{'mw'}"] # g -> mol,再除以 g/mol + + # mass -> vol + fil = df["mass"].notna() & df["rho"].notna() + df.loc[fil, "vol"] = df[fil]["mass"] / df[fil]["rho"] # g -> mL,再除以 g/mL + + # vol -> mass + fil = df["vol"].notna() & df["rho"].notna() + df.loc[fil, "mass"] = df[fil]["vol"] * df[fil]["rho"] # mL -> g,再乘以 g/mL + + # mass -> mol + fil = df["mw"].notna() & df["mass"].notna() + df.loc[fil, "mol"] = df[fil]["mass"] * 1000.0 / df[f"{'mw'}"] # g -> mol,再除以 g/mol + + eql = df[(df["eq"] > 0) & (df["mol"] > 0)] + if not st.session_state.get("no_check",False): + if eql.size > 1: + st.error("对于当量存在物质,只允许一个物质设置用量、质量或体积") + raise ValueError + if eql.size == 0 and not df[(df["eq"] > 0)].empty: + st.error("设置了当量,但是均没有设置用量、质量或体积") + raise ValueError + if not eql.empty: + ref = eql.iloc[0] + base = ref["mol"]/ref["eq"] + eqfil = df["eq"] > 0 + df.loc[eqfil, "mol"] = df[eqfil]["eq"] * base + + fil = df["mw"].notna() & eqfil + df.loc[fil, "mass"] = df[fil]["mol"] * df[f"{'mw'}"] / 1000.0 # mmol -> mol,再乘以 g/mol + + fil = df["rho"].notna() & df["mass"].notna() & eqfil + df.loc[fil, "vol"] = df[fil]["mass"] / df[fil]["rho"] # g -> mL,再除以 g/mL + + df.columns = [ + "物质", + "分子量", + "当量", + "用量(mmol)", + "质量(g)", + "密度(g/mL)", + "体积(mL)", + "备注" + ] + st.session_state.reaction_data = df + + except Exception as e: + st.error("计算过程中出错,表格可能有误") + raise e + print("calc_reaction_data error:", e) + return + def recalculate_reaction_data(): """根据最近一次编辑的行及当量,推算其他未编辑行的用量,并更新质量/体积。""" try: @@ -382,17 +465,35 @@ def recalculate_reaction_data(): # 基准行的自洽计算(用量/质量/体积) brow = df.loc[basis_idx] + + if "物质" in edited.keys() and pd.isna(brow["分子量"]) and "分子量" not in edited.keys(): + try: + mass = molmass.Formula(edited["物质"]).mass + edited["分子量"] = mass + df.loc[basis_idx, "分子量"] = mass + except Exception as e: + pass + if "密度(g/mL)" in edited.keys(): - df.loc[basis_idx, "密度(g/mL)"] = edited["密度(g/mL)"] - if brow.get("体积(mL)") is None and "质量(g)" in brow.keys(): + if _to_float(brow.get("体积(mL)")) is None and "质量(g)" in brow.keys(): edited["质量(g)"] = _to_float(brow.get("质量(g)")) - elif brow.get("质量(g)") is None and "体积(mL)" in brow.keys(): + elif _to_float(brow.get("质量(g)")) is None and "体积(mL)" in brow.keys(): edited["体积(mL)"] = _to_float(brow.get("体积(mL)")) else: + print(brow) st.error("当质量和体积同时存在时,修改密度为未定义行为。") - st.warning("由于计算失败,当前表格内容可能存在错误。") - return + raise ValueError + + if "分子量" in edited.keys(): + if all(pd.notna(brow[["质量(g)","用量(mmol)"]])): + st.error("当质量和用量同时存在时,修改分子量为未定义行为。") + raise ValueError + if pd.notna(brow.get("质量(g)")): + edited["质量(g)"] = _to_float(brow.get("质量(g)")) + if pd.notna(brow.get("用量(mmol)")): + edited["用量(mmol)"] = _to_float(brow.get("用量(mmol)")) + b_mw = _to_float(brow.get("分子量")) b_density = edited.get("密度(g/mL)", _to_float(brow.get("密度(g/mL)"))) b_amount = edited.get("用量(mmol)", None) @@ -459,7 +560,7 @@ def recalculate_reaction_data(): st.session_state.reaction_data = df except Exception as e: # raise e - st.error("重新计算反应数据时出错,表格可能有误") + st.warning("重新计算反应数据时出错,表格可能有误") print("recalculate_reaction_data error:", e) def add_compound_to_reaction(compound:PubChemCompound): From 9c92a3a44955a1baf6c1e8ec4ea5df9ae5587d4d Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Wed, 20 Aug 2025 09:50:45 +0800 Subject: [PATCH 10/15] fix --- mw_tool/pubchem_tool.py | 36 +++++++++++++++++------------------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/mw_tool/pubchem_tool.py b/mw_tool/pubchem_tool.py index ed647fa..4a9b3d0 100644 --- a/mw_tool/pubchem_tool.py +++ b/mw_tool/pubchem_tool.py @@ -3,12 +3,10 @@ import streamlit as st import pubchempy as pcp import re from typing import Optional, Dict, List, cast -from io import BytesIO -import base64 -from PIL import Image import requests import pandas as pd import numpy as np +import traceback class PubChemCompound: def __init__(self, @@ -293,12 +291,13 @@ def reaction_table_page(): st.session_state.reaction_data = edited_data st.session_state.reaction_table_refresh = 2 st.rerun() - - # print(st.session_state.reaction_data) - # 仅当返回的是 DataFrame 时再回写;如果是变更字典则由回调处理 - # if isinstance(edited_data, pd.DataFrame): - # print("Edited DataFrame:", edited_data) - # st.session_state.reaction_data = edited_data + +def get_mass_safe(chemical_name: str) -> float: + try: + mass = molmass.Formula(chemical_name).mass + return mass + except Exception as e: + return float("nan") def calc_reaction_data(): try: @@ -317,6 +316,9 @@ def calc_reaction_data(): st.error("质量、体积和密度不能同时存在或可求") raise ValueError + fil = df["mw"].isna() & df["name"].notna() + df.loc[fil, "mw"] = df[fil]["name"].apply(get_mass_safe) + # mol -> mass fil = df["mw"].notna() & df["mol"].notna() df.loc[fil, "mass"] = df[fil]["mol"] * df[f"{'mw'}"] / 1000.0 # mmol -> mol,再乘以 g/mol @@ -339,10 +341,10 @@ def calc_reaction_data(): eql = df[(df["eq"] > 0) & (df["mol"] > 0)] if not st.session_state.get("no_check",False): - if eql.size > 1: + if eql.shape[0] > 1: st.error("对于当量存在物质,只允许一个物质设置用量、质量或体积") raise ValueError - if eql.size == 0 and not df[(df["eq"] > 0)].empty: + if eql.shape[0] == 0 and not df[(df["eq"] > 0)].empty: st.error("设置了当量,但是均没有设置用量、质量或体积") raise ValueError if not eql.empty: @@ -370,9 +372,10 @@ def calc_reaction_data(): st.session_state.reaction_data = df except Exception as e: + if "df" in locals(): + st.session_state.reaction_data = df st.error("计算过程中出错,表格可能有误") - raise e - print("calc_reaction_data error:", e) + print("calc_reaction_data error:", traceback.format_exc()) return def recalculate_reaction_data(): @@ -467,12 +470,7 @@ def recalculate_reaction_data(): brow = df.loc[basis_idx] if "物质" in edited.keys() and pd.isna(brow["分子量"]) and "分子量" not in edited.keys(): - try: - mass = molmass.Formula(edited["物质"]).mass - edited["分子量"] = mass - df.loc[basis_idx, "分子量"] = mass - except Exception as e: - pass + df.loc[basis_idx, "分子量"] = get_mass_safe(edited["物质"]) if "密度(g/mL)" in edited.keys(): if _to_float(brow.get("体积(mL)")) is None and "质量(g)" in brow.keys(): From 78b82192a13ae9cca5c67b89be7e5eda586e7cf0 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Wed, 20 Aug 2025 10:27:38 +0800 Subject: [PATCH 11/15] update --- mw_tool/{main.py => main_old.py} | 56 +-------------- mw_tool/pubchem_tool.py | 120 +++++++++++++++++++------------ 2 files changed, 77 insertions(+), 99 deletions(-) rename mw_tool/{main.py => main_old.py} (92%) diff --git a/mw_tool/main.py b/mw_tool/main_old.py similarity index 92% rename from mw_tool/main.py rename to mw_tool/main_old.py index b8449b4..bcf1511 100644 --- a/mw_tool/main.py +++ b/mw_tool/main_old.py @@ -60,31 +60,6 @@ def calculate_molecular_weight_from_smiles(smiles): st.error(f"SMILES分子量计算错误: {str(e)}") return None -def generate_molecule_image(inchi=None, smiles=None): - """从SMILES生成分子结构图""" - return None - try: - if inchi: - mol = Chem.MolFromInchi(inchi) - elif smiles: - mol = Chem.MolFromSmiles(smiles) - else: - st.error("必须提供InChI或SMILES字符串") - return None - if mol: - # 生成分子图像 - img = Draw.MolToImage(mol, size=(300, 300)) - # 将图像转换为字节流 - img_buffer = BytesIO() - img.save("a.png") - img.save(img_buffer, format='PNG') - img_buffer.seek(0) - return img_buffer - else: - return None - except Exception as e: - st.error(f"分子结构图生成错误: {str(e)}") - return None def get_pubchem_properties(compound): """从PubChem获取密度、熔点、沸点信息""" @@ -144,16 +119,6 @@ def get_pubchem_properties(compound): 'boiling_point': None } -def is_liquid_at_room_temp(melting_point): - """判断常温下是否为液体(假设常温为25°C)""" - if melting_point is None: - return False - try: - mp = float(melting_point) - return mp < 25 # 熔点低于25°C认为是液体 - except: - return False - def sync_calculations(compound_data, mmol=None, mass=None, volume=None, changed_field=None): """同步计算mmol、质量、体积""" if not compound_data: @@ -315,18 +280,7 @@ with col2: st.markdown("### 其他数据") st.page_link(f"https://pubchem.ncbi.nlm.nih.gov/compound/{data['cid']}",label="**访问PubChem**") st.image(f"https://pubchem.ncbi.nlm.nih.gov/image/imgsrv.fcgi?cid={data['cid']}&t=s","结构式") - # st.button("访问PubChem",on_click=lambda :st.dire) - # if data['melting_point']: - # st.metric("熔点 (°C)", data['melting_point']) - # # 显示分子结构图 - # if data.get('inchi') or data.get('smiles'): - # st.markdown("分子结构图") - # mol_img = generate_molecule_image(inchi=data['inchi'], smiles=data['smiles']) - # if mol_img: - # st.image(mol_img, caption="分子键线式结构图", width=150) - # else: - # st.info("无法生成分子结构图") - + # 添加熔沸点信息的展开区域 if data.get('melting_point_src') or data.get('boiling_point_src'): with st.expander("熔沸点信息", expanded=False): @@ -357,9 +311,6 @@ with col2: melting_point = re.search(r'\d*\.\d+', melting_data[0]) if melting_point: melting_point = float(melting_point.group()) - is_liquid = is_liquid_at_room_temp(melting_point) - else: - is_liquid = False # 检测值变化并执行计算 def handle_change(field_name, new_value, current_value): @@ -427,10 +378,7 @@ with col2: # 密度显示选项 show_density = False if data['density_src']: - if is_liquid: - show_density = st.checkbox("显示密度信息", value=True) - else: - show_density = st.checkbox("显示密度信息", value=False) + show_density = st.checkbox("显示密度信息", value=False) if show_density: import re diff --git a/mw_tool/pubchem_tool.py b/mw_tool/pubchem_tool.py index 4a9b3d0..71bbf5a 100644 --- a/mw_tool/pubchem_tool.py +++ b/mw_tool/pubchem_tool.py @@ -203,8 +203,39 @@ def calculate_properties(molecular_weight: float, amount_mmol: Optional[float] = def reaction_table_page(): """反应表格页面""" - st.header("⚗️ 反应表格") - + st.header("反应表格") + + with st.expander("**使用说明**", expanded=False): + st.markdown('''在下方的表格中,您可以输入反应数据,程序会尝试计算其余各项。当量为0时,该物质不参与当量计算。 +### 立即计算 + +选中`立即计算`选项后,输入后立即尝试计算修改部分,您可以实时查看计算结果。但请注意,请勿同时修改多项内容。如果输入某项后计算未正确进行,这是潜在的缺陷,请反馈。 + +不保证在您主动修改计算结果后程序能正确处理(例如,程序算出质量后您主动修改),通常这会伴随着警告,但也有可能遗漏某些情况。 + +### 延迟计算 + +不选中`立即计算`后,在完成输入后,您可以点击“计算”按钮,程序会尝试计算所有可用的结果。 + +程序在计算时会检查是否有冗余数据,例如同时给定分子量、用量、质量,并拒绝在这种情况下计算。您可以选中`不使用检查`选项来禁用检查,但请保证数据自洽(即使得所有已知量之间的关系成立),否则会发生未定义行为。 + +如果计算后您想要修改或补充内容再次计算,请选中`不使用检查`选项。 +''') + with st.expander("关于未定义行为"): + st.markdown('''程序内部计算顺序为 +1. n -> mass +2. mass -> n +3. mass -> volumn +4. volumn -> mass +5. mass -> n +6. eqiv -> n +7. n -> mass +8. mass -> volumn + +如果存在不自洽的数据,您可以通过上述内容预测程序行为。但这不应该在实际使用中依赖,因为在不同版本下实现可能发生变化。 + +''') + # 初始化数据 if 'reaction_data' not in st.session_state: df = pd.DataFrame([[None,None,None,None,None,None,None,None]],columns=[ @@ -222,12 +253,11 @@ def reaction_table_page(): st.session_state.reaction_data = df - st.write("### 反应物质表格") - use_on_change = st.checkbox("是否立即计算", value=True) - st.info(f"💡 当量为0时,该物质不参与当量计算。"+("如果保证表格已有内容自洽,可选中不使用检查。" if not use_on_change else "")) + st.info(f"💡 当量为0时,该物质不参与当量计算。") + use_on_change = st.checkbox("立即计算", value=True) if not use_on_change: - st.button("计算",on_click=calc_reaction_data) st.checkbox("不使用检查", value=False,key="no_check",) + st.button("计算",on_click=calc_reaction_data,type="primary") # 使用data_editor创建可编辑表格 edited_data = st.data_editor( @@ -359,7 +389,17 @@ def calc_reaction_data(): fil = df["rho"].notna() & df["mass"].notna() & eqfil df.loc[fil, "vol"] = df[fil]["mass"] / df[fil]["rho"] # g -> mL,再除以 g/mL - df.columns = [ + + + except Exception as e: + # if "df" in locals(): + # st.session_state.reaction_data = df + st.error("计算过程中出错,表格可能有误") + print("calc_reaction_data error:", traceback.format_exc()) + return + finally: + if "df" in locals(): + df.columns = [ "物质", "分子量", "当量", @@ -368,15 +408,8 @@ def calc_reaction_data(): "密度(g/mL)", "体积(mL)", "备注" - ] - st.session_state.reaction_data = df - - except Exception as e: - if "df" in locals(): + ] st.session_state.reaction_data = df - st.error("计算过程中出错,表格可能有误") - print("calc_reaction_data error:", traceback.format_exc()) - return def recalculate_reaction_data(): """根据最近一次编辑的行及当量,推算其他未编辑行的用量,并更新质量/体积。""" @@ -579,8 +612,31 @@ def add_compound_to_reaction(compound:PubChemCompound): def compound_search_page(): """化合物搜索页面""" # 输入区域 - st.header("📝 输入查询条件") - + + + with st.expander("**使用说明**"): + st.markdown(""" + 1. **选择搜索类型**: + - 名称: 输入化合物的常用名称或IUPAC名称 + - 化学式: 输入分子式 (如 C2H6O) + - SMILES: 输入SMILES字符串 (如 CCO) + + 2. **输入查询条件**: 在输入框中输入相应的查询词 + + 3. **点击搜索**: 系统将从PubChem数据库中查询匹配的化合物 + + 4. **查看结果**: + - 基本信息包括名称、化学式、分子量和2D结构图 + - 密度和熔沸点信息可在展开区域查看 + - 计算器可帮助您计算用量、质量和体积的关系 + + ### 示例查询 + - **名称**: ethanol, water, glucose + - **化学式**: C2H6O, H2O, C6H12O6 + - **SMILES**: CCO, O, C(C1C(C(C(C(O1)O)O)O)O)O + """) + + st.header("查询条件") col1, col2 = st.columns([1, 2]) with col1: @@ -802,45 +858,19 @@ def compound_search_page(): st.rerun() else: st.warning("无分子量数据,无法进行计算") + - else: - # 显示使用说明 - st.info("👈 请在左侧输入化合物信息开始查询") - - st.markdown(""" - ### 🔍 使用说明 - - 1. **选择搜索类型**: - - 名称: 输入化合物的常用名称或IUPAC名称 - - 化学式: 输入分子式 (如 C2H6O) - - SMILES: 输入SMILES字符串 (如 CCO) - - 2. **输入查询条件**: 在输入框中输入相应的查询词 - - 3. **点击搜索**: 系统将从PubChem数据库中查询匹配的化合物 - - 4. **查看结果**: - - 基本信息包括名称、化学式、分子量和2D结构图 - - 密度和熔沸点信息可在展开区域查看 - - 计算器可帮助您计算用量、质量和体积的关系 - - ### 📝 示例查询 - - **名称**: ethanol, water, glucose - - **化学式**: C2H6O, H2O, C6H12O6 - - **SMILES**: CCO, O, C(C1C(C(C(C(O1)O)O)O)O)O - """) def main(): st.set_page_config( - page_title="PubChem化合物查询工具", + page_title="有机合成用量计算工具", page_icon="🧪", layout="wide" ) # 侧边栏导航 with st.sidebar: - st.title("🧪 化学工具") page = st.radio( "选择功能页面", ["化合物查询", "反应表格"], From 531389177195d8d1d6e409aca691e87e0169aa86 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Wed, 20 Aug 2025 10:27:58 +0800 Subject: [PATCH 12/15] use new ver --- mw_tool/{pubchem_tool.py => main.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename mw_tool/{pubchem_tool.py => main.py} (100%) diff --git a/mw_tool/pubchem_tool.py b/mw_tool/main.py similarity index 100% rename from mw_tool/pubchem_tool.py rename to mw_tool/main.py From cddd5d7e3b35cedcb20e34536dcf1822e89ec4a3 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Wed, 20 Aug 2025 10:31:41 +0800 Subject: [PATCH 13/15] mw_tool fix req --- mw_tool/requirements.txt | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mw_tool/requirements.txt b/mw_tool/requirements.txt index 69b682a..8332154 100644 --- a/mw_tool/requirements.txt +++ b/mw_tool/requirements.txt @@ -1,3 +1,5 @@ streamlit>=1.28.0 pubchempy>=1.0.4 -rdkit>=2022.9.5 +requests>=2.25.0 +Pillow>=8.0.0 +molmass \ No newline at end of file From f1f71bcc4b7a2c1f48ac5e4e6b42d0b132440984 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Wed, 20 Aug 2025 10:45:32 +0800 Subject: [PATCH 14/15] fix melt and boil col --- mw_tool/main.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/mw_tool/main.py b/mw_tool/main.py index 71bbf5a..3d6e2ed 100644 --- a/mw_tool/main.py +++ b/mw_tool/main.py @@ -775,13 +775,13 @@ def compound_search_page(): else: st.warning("未找到熔点数据") - with col2: - st.subheader("沸点") - if compound.boiling_point: - for bp in compound.boiling_point: - st.write(f"• {bp}") - else: - st.warning("未找到沸点数据") + with col2: + st.subheader("沸点") + if compound.boiling_point: + for bp in compound.boiling_point: + st.write(f"• {bp}") + else: + st.warning("未找到沸点数据") # 计算器 st.markdown("---") From 9d3c263683cf8ab0d866b2be4eb182f896464933 Mon Sep 17 00:00:00 2001 From: flt6 <1404262047@qq.com> Date: Wed, 20 Aug 2025 10:47:17 +0800 Subject: [PATCH 15/15] fix melt and boil col --- mw_tool/main.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/mw_tool/main.py b/mw_tool/main.py index 3d6e2ed..c841f2a 100644 --- a/mw_tool/main.py +++ b/mw_tool/main.py @@ -769,11 +769,11 @@ def compound_search_page(): with col1: st.subheader("熔点") - if compound.melting_point: - for mp in compound.melting_point: - st.write(f"• {mp}") - else: - st.warning("未找到熔点数据") + if compound.melting_point: + for mp in compound.melting_point: + st.write(f"• {mp}") + else: + st.warning("未找到熔点数据") with col2: st.subheader("沸点")