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("Не удалось построить изоповерхности")