VLE / app.py
relativus's picture
Create app.py
8a7adc1 verified
import numpy as np
import pandas as pd
import plotly.graph_objects as go
from scipy.interpolate import LinearNDInterpolator
from scipy.spatial import Delaunay
import plotly.express as px
def barycentric_to_cartesian(df):
"""
Преобразует барицентрические координаты в декартовы для тетраэдра.
"""
# Вершины правильного тетраэдра
vertices = np.array([
[0, 0, 0], # v0
[1, 0, 0], # v1
[0.5, np.sqrt(3)/2, 0], # v2
[0.5, np.sqrt(3)/6, np.sqrt(6)/3] # v3
])
# Преобразование координат
bary_coords = df[['x1', 'x2', 'x3', 'x4']].values
cartesian_coords = np.dot(bary_coords, vertices)
result_df = df.copy()
result_df['x'] = cartesian_coords[:, 0]
result_df['y'] = cartesian_coords[:, 1]
result_df['z'] = cartesian_coords[:, 2]
return result_df
def add_tetrahedron_wireframe(fig, vertices):
"""Добавляет каркас тетраэдра и подписи вершин"""
edges = [
[0, 1], [0, 2], [0, 3],
[1, 2], [1, 3], [2, 3]
]
for edge in edges:
x_line = [vertices[edge[0]][0], vertices[edge[1]][0]]
y_line = [vertices[edge[0]][1], vertices[edge[1]][1]]
z_line = [vertices[edge[0]][2], vertices[edge[1]][2]]
fig.add_trace(go.Scatter3d(
x=x_line, y=y_line, z=z_line,
mode='lines',
line=dict(color='black', width=4),
showlegend=False
))
vertex_labels = ['V₁ (x₁=1)', 'V₂ (x₂=1)', 'V₃ (x₃=1)', 'V₄ (x₄=1)']
fig.add_trace(go.Scatter3d(
x=vertices[:, 0],
y=vertices[:, 1],
z=vertices[:, 2],
mode='text+markers',
text=vertex_labels,
textposition="top center",
marker=dict(size=8, color='red'),
showlegend=False
))
def create_tetrahedron_isosurfaces_correct(df, temperatures, resolution=60):
"""
Правильное построение изоповерхностей с помощью Marching Tetrahedra.
"""
df_cartesian = barycentric_to_cartesian(df)
# Вершины тетраэдра
vertices = np.array([
[0, 0, 0],
[1, 0, 0],
[0.5, np.sqrt(3)/2, 0],
[0.5, np.sqrt(3)/6, np.sqrt(6)/3]
])
# Создаем интерполятор в ДЕКАРТОВЫХ координатах
points = df_cartesian[['x', 'y', 'z']].values
temp_values = df_cartesian['T, C'].values
valid_mask = ~np.isnan(temp_values)
points = points[valid_mask]
temp_values = temp_values[valid_mask]
interpolator = LinearNDInterpolator(points, temp_values)
# Создаем регулярную сетку в барицентрических координатах
u = np.linspace(0, 1, resolution)
v = np.linspace(0, 1, resolution)
w = np.linspace(0, 1, resolution)
U, V, W = np.meshgrid(u, v, w, indexing='ij')
# Фильтруем точки внутри тетраэдра (u+v+w <= 1)
mask = (U + V + W) <= 1
U_valid = U[mask]
V_valid = V[mask]
W_valid = W[mask]
T_valid = 1 - U_valid - V_valid - W_valid
# Преобразуем в декартовы координаты
bary_coords = np.column_stack([U_valid, V_valid, W_valid, T_valid])
grid_points = np.dot(bary_coords, vertices)
# Интерполируем температуру в ДЕКАРТОВЫХ координатах
grid_temps = interpolator(grid_points)
# Создаем триангуляцию для регулярной сетки
print("Создаем триангуляцию для регулярной сетки...")
try:
tri = Delaunay(grid_points)
print(f"Создано {len(tri.simplices)} тетраэдров")
except Exception as e:
print(f"Ошибка триангуляции: {e}")
return None
fig = go.Figure()
for temp in temperatures:
print(f"Строим изоповерхность для {temp}°C")
vertices_list = []
faces_list = []
# Для каждого тетраэдра в сетке
for tetra in tri.simplices:
tetra_points = grid_points[tetra]
tetra_temps = grid_temps[tetra]
# Определяем, какие вершины выше/ниже изоповерхности
above = tetra_temps >= temp
below = tetra_temps < temp
# Если все вершины с одной стороны - пропускаем
if np.all(above) or np.all(below):
continue
# Находим пересечения изоповерхности с ребрами тетраэдра
intersections = []
intersection_edges = []
# Проверяем все ребра тетраэдра
edges = [(0,1), (0,2), (0,3), (1,2), (1,3), (2,3)]
for i, (idx1, idx2) in enumerate(edges):
temp1, temp2 = tetra_temps[idx1], tetra_temps[idx2]
point1, point2 = tetra_points[idx1], tetra_points[idx2]
# Если ребро пересекает изоповерхность
if (temp1 >= temp and temp2 < temp) or (temp1 < temp and temp2 >= temp):
# Линейная интерполяция
t = (temp - temp1) / (temp2 - temp1)
intersection_point = point1 + t * (point2 - point1)
intersections.append(intersection_point)
intersection_edges.append((idx1, idx2))
# Формируем полигоны в зависимости от количества пересечений
if len(intersections) == 3:
# Один треугольник
start_idx = len(vertices_list)
vertices_list.extend(intersections)
faces_list.append([start_idx, start_idx+1, start_idx+2])
elif len(intersections) == 4:
# Четырехугольник - разбиваем на 2 треугольника
start_idx = len(vertices_list)
vertices_list.extend(intersections)
# Первый вариант разбиения (диагональ 0-2)
faces_list.append([start_idx, start_idx+1, start_idx+2])
faces_list.append([start_idx, start_idx+2, start_idx+3])
# Альтернативный вариант разбиения (диагональ 1-3)
# faces_list.append([start_idx, start_idx+1, start_idx+3])
# faces_list.append([start_idx+1, start_idx+2, start_idx+3])
# Добавляем изоповерхность
if vertices_list:
vertices_array = np.array(vertices_list)
x = vertices_array[:, 0]
y = vertices_array[:, 1]
z = vertices_array[:, 2]
i_list, j_list, k_list = [], [], []
for face in faces_list:
i_list.append(face[0])
j_list.append(face[1])
k_list.append(face[2])
color_idx = temperatures.index(temp) % len(px.colors.qualitative.Set1)
fig.add_trace(go.Mesh3d(
x=x, y=y, z=z,
i=i_list, j=j_list, k=k_list,
color=px.colors.qualitative.Set1[color_idx],
opacity=0.7,
name=f'{temp}°C',
flatshading=True,
lighting=dict(diffuse=0.8, ambient=0.3),
lightposition=dict(x=100, y=100, z=100)
))
print(f"Добавлено {len(faces_list)} треугольников для температуры {temp}°C")
else:
print(f"Не найдено пересечений для температуры {temp}°C")
# Добавляем каркас тетраэдра
add_tetrahedron_wireframe(fig, vertices)
fig.update_layout(
title='Правильные изоповерхности в тетраэдре',
scene=dict(
xaxis_title='X',
yaxis_title='Y',
zaxis_title='Z',
aspectmode='data',
camera=dict(eye=dict(x=1.5, y=1.5, z=1.5))
),
width=1000,
height=800
)
return fig
# 🎯 Создание тестовых данных
def generate_test_data(n_points=100):
"""
Генерирует случайные барицентрические координаты и температуры
"""
# Генерируем случайные барицентрические координаты
# x1 + x2 + x3 + x4 = 1, все >= 0
data = np.random.dirichlet([1, 1, 1, 1], size=n_points)
# Создаем DataFrame
df = pd.DataFrame(data, columns=['x1', 'x2', 'x3', 'x4'])
# Генерируем температуры в разумном диапазоне
# Например, температура зависит от барицентрических координат
df['T, C'] = 50 + 30 * (df['x1'] * 0.5 + df['x2'] * 0.3 + df['x3'] * 0.2 + df['x4'] * 0.1) + \
np.random.normal(0, 2, size=n_points) # добавляем шум
return df
# 🚀 Основной код для Streamlit
if __name__ == "__main__":
import streamlit as st
st.title("Изоповерхности в тетраэдре")
# 📁 Загрузка Excel-файла
uploaded_file = st.file_uploader(
"Загрузите Excel файл с данными (x1, x2, x3, x4, T, C)",
type=["xlsx", "xls"],
help="Файл должен содержать столбцы: x1, x2, x3, x4, T, C"
)
if uploaded_file is not None:
# Читаем Excel-файл
try:
data = pd.read_excel(uploaded_file)
st.success("Файл успешно загружен!")
# Проверяем, что все нужные столбцы есть
required_columns = ['x1', 'x2', 'x3', 'x4', 'T, C']
missing_cols = [col for col in required_columns if col not in data.columns]
if missing_cols:
st.error(f"⚠️ В файле отсутствуют следующие столбцы: {missing_cols}")
st.stop()
st.write("Пример данных из файла:")
st.dataframe(data.head(10))
except Exception as e:
st.error(f"Ошибка при чтении файла: {e}")
st.stop()
else:
# Если файл не загружен - используем тестовые данные
st.info("Файл не загружен. Используются тестовые данные.")
data = generate_test_data(n_points=200)
# Параметры
available_temps = sorted(data['T, C'].dropna().unique())
if len(available_temps) > 0:
default_temps = [available_temps[0]] if len(available_temps) == 1 else [available_temps[0], available_temps[-1]]
else:
default_temps = [64.5, 81]
target_temperatures = st.multiselect(
"Выберите температуры для изоповерхностей",
options=sorted(data['T, C'].dropna().unique()),
default=default_temps
)
resolution = st.slider("Разрешение сетки", min_value=20, max_value=100, value=60)
if st.button("Построить изоповерхности"):
with st.spinner("Идет построение..."):
fig = create_tetrahedron_isosurfaces_correct(data, target_temperatures, resolution=resolution)
if fig:
st.plotly_chart(fig, use_container_width=True)
else:
st.error("Не удалось построить изоповерхности")