File size: 12,457 Bytes
8a7adc1
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
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("Не удалось построить изоповерхности")