forked from flt/tools
634 lines
28 KiB
Python
634 lines
28 KiB
Python
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 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获取密度、熔点、沸点信息"""
|
||
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 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:
|
||
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","结构式")
|
||
# 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):
|
||
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())
|
||
is_liquid = is_liquid_at_room_temp(melting_point)
|
||
else:
|
||
is_liquid = False
|
||
|
||
# 检测值变化并执行计算
|
||
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']:
|
||
if is_liquid:
|
||
show_density = st.checkbox("显示密度信息", value=True)
|
||
else:
|
||
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("请在左侧输入要查询的化学物质")
|