Merge branch 'master' of flt6.top:flt/tools

This commit is contained in:
2025-08-26 20:02:27 +08:00
6 changed files with 1606 additions and 559 deletions

2
.gitignore vendored
View File

@ -7,3 +7,5 @@ __pycache__
*.mp4
*.log
test
.venv
uv.lock

199
cord/main.py Normal file
View File

@ -0,0 +1,199 @@
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):
"""
绘制反应坐标图
"""
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*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*K_POS[i]
ax1.annotate(str(line["Name"]), p, ha='center')
last = p1
ax1.set_xlabel("Reaction Coordinate")
ax1.xaxis.set_ticks([])
ax1.set_ylabel("Energy (kcal/mol)")
ax1.set_ylim(miny-varyy*0.1, maxy+varyy*0.1)
return fig,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
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()
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,K_POS
out_file = io.BytesIO()
st.set_page_config(
page_title="反应坐标绘制",
page_icon=":chart_with_upwards_trend:",
layout="wide",
initial_sidebar_state="expanded"
)
st.title("反应坐标绘制")
st.write("---")
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,K_POS = load_data(file)
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",
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)
)
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)

4
cord/requirements.txt Normal file
View File

@ -0,0 +1,4 @@
matplotlib>=3.10.5
openpyxl>=3.1.5
pandas>=2.3.1
streamlit>=1.47.1

File diff suppressed because it is too large Load Diff

581
mw_tool/main_old.py Normal file
View File

@ -0,0 +1,581 @@
import streamlit as st
import pubchempy as pcp
from rdkit import Chem
from rdkit.Chem import rdMolDescriptors
import requests
from io import BytesIO
st.set_page_config(
page_title="质量及密度查询",
layout="wide"
)
# 初始化 session state
if 'compound_data' not in st.session_state:
st.session_state.compound_data = None
def search_compound(query):
"""搜索化合物信息"""
try:
compounds = None
try:
comp = Chem.MolFromSmiles(query)
if comp:
compounds = pcp.get_compounds(query, 'smiles', listkey_count=3)
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 isinstance(compounds, list) and len(compounds) > 0:
st.info("成功查询物质基本信息,正在获取更多数据。")
return compounds[0]
else:
return None
except Exception as e:
st.error(f"搜索时发生错误: {str(e)}")
# raise e
return None
def calculate_molecular_weight_from_smiles(smiles):
"""从SMILES计算分子量"""
try:
mol = Chem.MolFromSmiles(smiles)
if mol:
return rdMolDescriptors.CalcExactMolWt(mol)
else:
return None
except Exception as e:
st.error(f"SMILES分子量计算错误: {str(e)}")
return None
def get_pubchem_properties(compound):
"""从PubChem获取密度、熔点、沸点信息"""
try:
# 初始化返回数据
properties = {
'density': None,
'melting_point': None,
'boiling_point': None
}
cid = compound.cid
# 尝试获取物理化学性质相关的记录
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 sync_calculations(compound_data, mmol=None, mass=None, volume=None, changed_field=None):
"""同步计算mmol、质量、体积"""
if not compound_data:
return mmol, mass, volume
# 确保数值类型转换
try:
molecular_weight = float(compound_data.get('molecular_weight', 0))
density_select = compound_data.get('density_select', None)
density = float(density_select) if density_select is not None else None
except (ValueError, TypeError):
st.error("分子量或密度数据格式错误,无法进行计算")
return mmol, mass, volume
if molecular_weight == 0:
return mmol, mass, volume
try:
if changed_field == 'mmol' and mmol is not None:
# 根据mmol计算质量
mass = (mmol / 1000) * molecular_weight # mmol转mol再乘分子量
# 如果有密度,计算体积
if density and density > 0:
volume = mass / density
elif changed_field == 'mass' and mass is not None:
# 根据质量计算mmol
mmol = (mass / molecular_weight) * 1000 # 质量除分子量得mol再转mmol
# 如果有密度,计算体积
if density and density > 0:
volume = mass / density
elif changed_field == 'volume' and volume is not None and density and density > 0:
# 根据体积计算质量
mass = volume * density
# 根据质量计算mmol
mmol = (mass / molecular_weight) * 1000
except Exception as e:
st.error(f"计算错误: {str(e)}")
return mmol, mass, volume
# 主界面
col1, col2 = st.columns([1, 2])
with col1:
st.subheader("物质查询")
query = st.text_input("输入化学式、名称或SMILES:", placeholder="例如: H2O, water, CCO")
# 添加直接计算分子量选项
calc_mw_only = st.checkbox("仅计算分子量(不查询数据库)", help="勾选此项将跳过数据库查询仅从SMILES计算分子量")
if st.button("查询" if not calc_mw_only else "计算", type="primary"):
if query:
with st.spinner("正在处理..."):
# 如果选择仅计算分子量直接从SMILES计算
if calc_mw_only:
mol_weight = calculate_molecular_weight_from_smiles(query)
if mol_weight:
compound_data = {
'name': "用户输入化合物",
'formula': "从SMILES计算",
'molecular_weight': mol_weight,
'melting_point': None,
'density_src': None,
'melting_point_src': None,
'boiling_point_src': None,
'smiles': query,
"inchi": None,
'found': False
}
st.session_state.compound_data = compound_data
st.success("分子量计算完成!")
else:
st.error("输入的SMILES格式无效")
st.session_state.compound_data = None
else:
# 原有的查询逻辑
compound = search_compound(query)
if compound is not None:
# 查询到化合物
# 获取PubChem的物理化学性质信息
pubchem_properties = get_pubchem_properties(compound)
compound_data = {
'name': compound.iupac_name,
'cid': compound.cid,
'formula': compound.molecular_formula,
'molecular_weight': compound.molecular_weight,
"density_src": pubchem_properties['density'],
'melting_point_src': pubchem_properties['melting_point'],
'boiling_point_src': pubchem_properties['boiling_point'],
'smiles': compound.canonical_smiles,
"inchi": compound.inchi if hasattr(compound, 'inchi') else None,
'found': True,
}
st.session_state.compound_data = compound_data
# 显示查询结果信息
if compound_data['density_src'] or compound_data['melting_point_src'] or compound_data['boiling_point_src']:
properties_found = []
if compound_data['density_src']:
properties_found.append("密度")
if compound_data['melting_point_src']:
properties_found.append("熔点")
if compound_data['boiling_point_src']:
properties_found.append("沸点")
st.success(f"查询成功!(找到{', '.join(properties_found)}信息)")
else:
st.success("查询成功!(未找到物理性质信息)")
else:
# 未查询到检查是否为SMILES
if query:
mol_weight = calculate_molecular_weight_from_smiles(query)
if mol_weight:
compound_data = {
'name': "未知化合物",
'formula': "从SMILES计算",
'molecular_weight': mol_weight,
'melting_point': None,
'density_src': None,
'melting_point_src': None,
'boiling_point_src': None,
'smiles': query,
"inchi": None,
'found': False
}
st.session_state.compound_data = compound_data
st.warning("⚠️ 未在数据库中找到但已从SMILES计算分子量")
else:
st.error("❌ 未找到该化合物且SMILES格式无效")
st.session_state.compound_data = None
with col2:
st.subheader("化合物信息")
if st.session_state.compound_data:
data = st.session_state.compound_data
# 显示基本信息
info_col1, info_col2 = st.columns(2)
with info_col1:
st.metric("物质名称", data['name'])
try:
molecular_weight_value = float(data['molecular_weight'])
st.metric("分子量 (g/mol)", f"{molecular_weight_value:.3f}")
except (ValueError, TypeError):
st.metric("分子量 (g/mol)", "数据格式错误")
with info_col2:
st.metric("化学式", data['formula'])
if data.get("cid"):
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","结构式")
# 添加熔沸点信息的展开区域
if data.get('melting_point_src') or data.get('boiling_point_src'):
with st.expander("熔沸点信息", expanded=False):
col1, col2 = st.columns(2)
with col1:
if data.get('melting_point_src'):
st.markdown("### 熔点数据")
melting_data = data['melting_point_src']
if isinstance(melting_data, list):
for i, mp in enumerate(melting_data, 1):
st.write(f"{i}. {mp}")
else:
st.write(melting_data)
with col2:
if data.get('boiling_point_src'):
st.markdown("### 沸点数据")
boiling_data = data['boiling_point_src']
if isinstance(boiling_data, list):
for i, bp in enumerate(boiling_data, 1):
st.write(f"{i}. {bp}")
else:
st.write(boiling_data)
# 判断是否为液体
melting_data = data['melting_point_src']
if isinstance(melting_data, list) and len(melting_data) > 0:
import re
melting_point = re.search(r'\d*\.\d+', melting_data[0])
if melting_point:
melting_point = float(melting_point.group())
# 检测值变化并执行计算
def handle_change(field_name, new_value, current_value):
try:
# 确保都转换为浮点数
new_value = float(new_value) if new_value is not None else 0.0
current_value = float(current_value) if current_value is not None else 0.0
if abs(new_value - current_value) > 1e-6: # 避免浮点数比较问题
# 同步计算 - 确保数据类型正确
try:
calc_data = {
'molecular_weight': float(data['molecular_weight']),
'density_select': float(st.session_state.get('density_select', 0)) if (show_density and st.session_state.get('density_select')) else None
}
except (ValueError, TypeError):
st.error("化合物数据格式错误,无法进行计算")
return
mmol_calc = mass_calc = volume_calc = 0.0
if field_name == 'mmol':
mmol_calc, mass_calc, volume_calc = sync_calculations(
calc_data, new_value, None, None, 'mmol'
)
elif field_name == 'mass':
mmol_calc, mass_calc, volume_calc = sync_calculations(
calc_data, None, new_value, None, 'mass'
)
elif field_name == 'volume':
mmol_calc, mass_calc, volume_calc = sync_calculations(
calc_data, None, None, new_value, 'volume'
)
elif field_name == 'density':
# 密度变化时,如果已有质量,重新计算体积;如果已有体积,重新计算质量
current_mass = st.session_state.mass_val
current_volume = st.session_state.volume_val
if current_mass > 0:
# 根据质量重新计算体积
mmol_calc, mass_calc, volume_calc = sync_calculations(
calc_data, None, current_mass, None, 'mass'
)
elif current_volume > 0:
# 根据体积重新计算质量
mmol_calc, mass_calc, volume_calc = sync_calculations(
calc_data, None, None, current_volume, 'volume'
)
else:
return # 没有质量或体积数据,无需重新计算
# 更新session state
st.session_state.mmol_val = float(mmol_calc) if mmol_calc is not None else 0.0
st.session_state.mass_val = float(mass_calc) if mass_calc is not None else 0.0
st.session_state.volume_val = float(volume_calc) if volume_calc is not None else 0.0
st.session_state.last_changed = field_name
# 强制刷新页面以更新输入框的值
if field_name != 'density': # 密度变化时不需要rerun因为已经在密度输入处理中rerun了
st.rerun()
except (ValueError, TypeError) as e:
st.error(f"数值转换错误: {str(e)}")
return
# 密度显示选项
show_density = False
if data['density_src']:
show_density = st.checkbox("显示密度信息", value=False)
if show_density:
import re
# 初始化密度值在session state中
if 'density_select' not in st.session_state:
st.session_state.density_select = None
if 'density_input_value' not in st.session_state:
st.session_state.density_input_value = 0.0
density_data = data['density_src']
# print(density_data)
# 如果密度是列表且长度>1让用户选择
if isinstance(density_data, list) and len(density_data) > 1:
st.markdown("**选择密度数据:**")
# 为每个密度选项提取数值并显示
density_options = []
density_values = []
for i, density_str in enumerate(density_data):
# 使用正则表达式提取密度数值
match = re.search(r'\d*\.\d+', str(density_str))
if match:
extracted_value = float(match.group())
density_options.append(f"{extracted_value:.3f}: {density_str}")
density_values.append(extracted_value)
else:
density_options.append(f"0.000: {density_str} (无法提取数值)")
density_values.append(None)
# 用户选择密度
selected_index = st.selectbox(
"选择要使用的密度数据:",
range(len(density_options)),
format_func=lambda x: density_options[x],
key="density_selector"
)
# 获取选中的密度值
if density_values[selected_index] is not None:
selected_density_value = density_values[selected_index]
st.session_state.density_select = selected_density_value
# 显示并允许用户修改密度值
st.markdown("**密度值 (可修改)**")
new_density = st.number_input(
"密度 (g/mL)",
min_value=0.0,
value=float(st.session_state.density_select),
step=0.001,
format="%.3f",
key="density_input",
help="选择的密度值,可以手动修改"
)
# 检测密度值变化
if abs(new_density - st.session_state.density_input_value) > 1e-6:
st.session_state.density_select = new_density
st.session_state.density_input_value = new_density
# 更新compound_data中的密度值用于计算
st.session_state.compound_data['density_select'] = new_density
handle_change('density', 1, 0)
st.rerun()
else:
st.error("所选密度数据无法提取有效数值")
# 如果密度是单个值或列表长度为1
else:
try:
if isinstance(density_data, list):
density_str = str(density_data[0])
else:
density_str = str(density_data)
# 提取密度数值
match = re.search(r'\d*\.\d+', density_str)
if match:
density_value = float(match.group())
st.session_state.density_select = density_value
# 显示并允许用户修改密度值
st.markdown("**密度值 (可修改)**")
new_density = st.number_input(
"密度 (g/mL)",
min_value=0.0,
value=float(st.session_state.density_select),
step=0.001,
format="%.3f",
key="density_input_single",
help="提取的密度值,可以手动修改"
)
# 检测密度值变化
if abs(new_density - st.session_state.density_input_value) > 1e-6:
st.session_state.density_select = new_density
st.session_state.density_input_value = new_density
# 更新compound_data中的密度值用于计算
st.session_state.compound_data['density_select'] = new_density
handle_change('density', 1, 0)
st.rerun()
else:
st.error("无法从密度数据中提取有效数值")
except (ValueError, TypeError):
st.error("密度数据格式错误")
st.markdown("---")
# 计算器部分
st.subheader("用量计算器")
# 初始化值
if 'mmol_val' not in st.session_state:
st.session_state.mmol_val = 0.0
if 'mass_val' not in st.session_state:
st.session_state.mass_val = 0.0
if 'volume_val' not in st.session_state:
st.session_state.volume_val = 0.0
if 'last_changed' not in st.session_state:
st.session_state.last_changed = None
# 创建响应式列布局
density_select = st.session_state.get('density_select')
if show_density and density_select is not None:
calc_col1, calc_col2, calc_col3 = st.columns([1, 1, 1])
else:
calc_col1, calc_col2 = st.columns([1, 1])
calc_col3 = None
with calc_col1:
st.markdown("**物质的量**")
new_mmol = st.number_input(
"用量 (mmol)",
min_value=0.0,
value=float(st.session_state.mmol_val),
step=0.1,
format="%.3f",
key="mmol_input",
help="输入或计算得到的物质的量,单位:毫摩尔"
)
# 检测mmol变化
if st.session_state.last_changed != 'mmol':
handle_change('mmol', new_mmol, st.session_state.mmol_val)
with calc_col2:
st.markdown("**质量**")
new_mass = st.number_input(
"质量 (g)",
min_value=0.0,
value=float(st.session_state.mass_val),
step=0.001,
format="%.3f",
key="mass_input",
help="输入或计算得到的质量,单位:克"
)
# 检测mass变化
if st.session_state.last_changed != 'mass':
handle_change('mass', new_mass, st.session_state.mass_val)
if calc_col3 is not None:
with calc_col3:
st.markdown("**体积**")
new_volume = st.number_input(
"体积 (mL)",
min_value=0.0,
value=float(st.session_state.volume_val),
step=0.01,
format="%.3f",
key="volume_input",
help="输入或计算得到的体积,单位:毫升"
)
# 检测volume变化
if st.session_state.last_changed != 'volume':
handle_change('volume', new_volume, st.session_state.volume_val)
# 重置last_changed状态
st.session_state.last_changed = None
# 清零按钮
if st.button("清零所有数值", type="secondary"):
st.session_state.mmol_val = 0.0
st.session_state.mass_val = 0.0
st.session_state.volume_val = 0.0
st.session_state.last_changed = None
st.rerun()
st.session_state.mmol_val = 0.0
st.session_state.mass_val = 0.0
st.session_state.volume_val = 0.0
st.rerun()
else:
st.info("请在左侧输入要查询的化学物质")

View File

@ -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