|
|
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], |
|
|
[1, 0, 0], |
|
|
[0.5, np.sqrt(3)/2, 0], |
|
|
[0.5, np.sqrt(3)/6, np.sqrt(6)/3] |
|
|
]) |
|
|
|
|
|
|
|
|
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') |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
start_idx = len(vertices_list) |
|
|
vertices_list.extend(intersections) |
|
|
|
|
|
|
|
|
faces_list.append([start_idx, start_idx+1, start_idx+2]) |
|
|
faces_list.append([start_idx, 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): |
|
|
""" |
|
|
Генерирует случайные барицентрические координаты и температуры |
|
|
""" |
|
|
|
|
|
|
|
|
data = np.random.dirichlet([1, 1, 1, 1], size=n_points) |
|
|
|
|
|
|
|
|
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 |
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__": |
|
|
import streamlit as st |
|
|
|
|
|
st.title("Изоповерхности в тетраэдре") |
|
|
|
|
|
|
|
|
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: |
|
|
|
|
|
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("Не удалось построить изоповерхности") |
|
|
|