403Webshell
Server IP : 156.238.232.47  /  Your IP : 216.73.216.150
Web Server : nginx/1.25.3
System : Linux C202504152095410 3.10.0-1160.119.1.el7.x86_64 #1 SMP Tue Jun 4 14:43:51 UTC 2024 x86_64
User : www ( 1000)
PHP Version : 8.3.25
Disable Function : passthru,exec,system,putenv,chroot,chgrp,chown,shell_exec,popen,proc_open,pcntl_exec,ini_alter,ini_restore,dl,openlog,syslog,readlink,symlink,popepassthru,pcntl_alarm,pcntl_fork,pcntl_waitpid,pcntl_wait,pcntl_wifexited,pcntl_wifstopped,pcntl_wifsignaled,pcntl_wifcontinued,pcntl_wexitstatus,pcntl_wtermsig,pcntl_wstopsig,pcntl_signal,pcntl_signal_dispatch,pcntl_get_last_error,pcntl_strerror,pcntl_sigprocmask,pcntl_sigwaitinfo,pcntl_sigtimedwait,pcntl_exec,pcntl_getpriority,pcntl_setpriority,imap_open,apache_setenv
MySQL : OFF  |  cURL : ON  |  WGET : ON  |  Perl : ON  |  Python : ON  |  Sudo : ON  |  Pkexec : ON
Directory :  /www/data/wwwroot/python/

Upload File :
current_dir [ Writeable ] document_root [ Writeable ]

 

Command :


[ Back ]     

Current File : /www/data/wwwroot/python/app.py
# -*- coding: utf-8 -*-

"""

Solar Chimney Geometry Drawing & Calculation Program

Streamlit Run Command: streamlit run app.py

Updated: Move pipe control buttons (Outer+/Outer-/Inner+/Inner-) ONTO the chart image
         via CSS absolute positioning. Buttons overlay the right side of the matplotlib
         figure (Outer top / Inner bottom), clickable directly inside the chart area.
         All original functions preserved: exe calculation, dat file io, contact form,
         reference, session state, css style.

"""

import streamlit as st

import numpy as np

import matplotlib
matplotlib.use("Agg")   # 部署关键!!放在plt上面
import streamlit as st
import numpy as np
import matplotlib.pyplot as plt

from matplotlib.patches import Rectangle, Circle, FancyBboxPatch

import subprocess

import os

import csv

import io

from datetime import datetime

# ====================== Page Config ======================

st.set_page_config(

    page_title="Solar Chimney Geometry Model",

    page_icon="🔥",

    layout="wide",

    initial_sidebar_state="collapsed"

)

# ====================== Global CSS ======================

st.markdown("""

<style>

section[data-testid="stSidebar"],
div[data-testid="stSidebarCollapsedControl"] {
    display: none !important;
}

div[data-testid="stAppViewContainer"] > .main {
    margin-left: 0 !important;
}

/* 隐藏数字输入框的加减按钮(保持原有样式) */

div[data-testid="stNumberInput"] button[data-testid="stNumberInputDecrementButton"],

div[data-testid="stNumberInput"] button[data-testid="stNumberInputIncrementButton"],

div[data-testid="stNumberInput"] button.step-up,

div[data-testid="stNumberInput"] button.step-down {

    display: none !important;

    visibility: hidden !important;

    width: 0px !important;

    height: 0px !important;

    min-width: 0px !important;

    padding: 0px !important;

    margin: 0px !important;

    border: none !important;

}

div[data-testid="stNumberInput"] input::-webkit-inner-spin-button,

div[data-testid="stNumberInput"] input::-webkit-outer-spin-button {

    -webkit-appearance: none !important;

    appearance: none !important;

    margin: 0 !important;

    display: none !important;

}

div[data-testid="stNumberInput"] input[type="number"] {

    -moz-appearance: textfield !important;

}

div[data-testid="stNumberInput"] div[data-baseweb="input"] {

    width: 100% !important;

}

div[data-testid="stNumberInput"] {

    gap: 0px !important;

    margin-bottom: 0.3rem !important;

}

.stSidebar .stColumns {

    gap: 0.4rem !important;

}

.stSidebar [data-testid="stExpander"][aria-expanded="true"] summary {

    background-color: #ffe6e6;

    padding: 8px 10px;

    border-radius: 6px;

    font-weight: 600;

    font-size: 15px;

}

.stSidebar [data-testid="stExpander"][aria-expanded="false"] summary {

    background-color: #f0f2f6;

    padding: 6px 10px;

    border-radius: 6px;

    font-weight: 400;

    color: #555;

    font-size: 14px;

}

.stSidebar [data-testid="stExpander"][aria-expanded="true"]:nth-child(2) div {

    font-size: 0.9rem !important;

    color: #444;

}

.block-container {

    padding-top: 1rem !important;

    padding-left: 1rem !important;

    padding-right: 2rem !important;

}

div[data-testid="stAlert"] {

    max-width: 75% !important;

}

button[kind="primary"] {

    background-color: #ff3b3b !important;

    border: none !important;

}

button[kind="primary"]:hover {

    filter: brightness(0.92);

}

h1, h2, h3 {

    font-size: 24px !important;

    font-weight: bold !important;

    margin-top: 1.2rem !important;

    margin-bottom: 0.4rem !important;

}

hr {

    margin: 0.1rem 0 0.1rem 0 !important;

}

.block-container h3 {

    margin-top: 0.1rem !important;

    margin-bottom: 0rem !important;

}

.advanced-note {

    font-size: 0.8rem;

    color: #666;

}

div[data-testid="stImage"] {

    margin-top: -8px !important;

    margin-bottom: -8px !important;

}

.sidebar-small-note{

    font-size:0.75rem;

    color:#606060;

    margin-top:4px;

    line-height:1.4;

}

/* ================================================================ */

/* 图表内覆盖按钮:将 Outer/Inner 增删按钮绝对定位到图表图片右侧      */

/* ================================================================ */

/* 步骤1:让包含图片的垂直块变成相对定位(作为绝对定位的参考) */

div[data-testid="stVerticalBlock"] {

    position: relative !important;

}

/* 步骤2:定位紧跟在 stImage 后面的第一个和第二个水平按钮行 */

/* 第一行(Outer 管道)—— 对应图表右上区域(金色管道末端) */

div[data-testid="stImage"] + div[data-testid="stHorizontalBlock"] {

    position: absolute !important;

    right: 20px;                /* 距右侧边缘距离,可微调 */

    top: 10%;                   /* 根据实际管道位置调整 (约 10%~15%) */

    width: auto;                /* 宽度自适应按钮内容 */

    z-index: 100 !important;

    gap: 4px !important;        /* 按钮间距 */

}

/* 第二行(Inner 管道)—— 对应图表右中区域(蓝色管道末端) */

div[data-testid="stImage"] + div[data-testid="stHorizontalBlock"] + div[data-testid="stHorizontalBlock"] {

    position: absolute !important;

    right: 20px;

    top: 46%;                   /* 根据实际管道位置调整 (约 45%~50%) */

    width: auto;

    z-index: 100 !important;

    gap: 4px !important;

}

/* 覆盖按钮样式:半透明卡片感,小字号,悬停变红 */

div[data-testid="stImage"] + div[data-testid="stHorizontalBlock"] button,

div[data-testid="stImage"] + div[data-testid="stHorizontalBlock"] + div[data-testid="stHorizontalBlock"] button {

    font-size: 12px !important;

    padding: 4px 10px !important;

    background-color: rgba(255, 255, 255, 0.92) !important;

    border: 1px solid #cbd5e1 !important;

    border-radius: 6px !important;

    box-shadow: 0 1px 3px rgba(0,0,0,0.12) !important;

    color: #1e293b !important;

    white-space: nowrap;

}

div[data-testid="stImage"] + div[data-testid="stHorizontalBlock"] button:hover,

div[data-testid="stImage"] + div[data-testid="stHorizontalBlock"] + div[data-testid="stHorizontalBlock"] button:hover {

    background-color: #ff3b3b !important;

    color: #fff !important;

    border-color: #ff3b3b !important;

}

</style>

""", unsafe_allow_html=True)

# ====================== Session State Init ======================

if "n_out" not in st.session_state:

    st.session_state.n_out = 2

if "n_in" not in st.session_state:

    st.session_state.n_in = 3

if "Lo_list" not in st.session_state:

    st.session_state.Lo_list = [90, 90]

if "Do_list" not in st.session_state:

    st.session_state.Do_list = [18, 18]

if "Dco_list" not in st.session_state:

    st.session_state.Dco_list = [16]

if "Dco_len_list" not in st.session_state:

    st.session_state.Dco_len_list = [0]

if "Lin_list" not in st.session_state:

    st.session_state.Lin_list = [95, 95, 95]

if "Din_list" not in st.session_state:

    st.session_state.Din_list = [18, 18, 18]

if "Dci_list" not in st.session_state:

    st.session_state.Dci_list = [16, 16]

if "Dci_len_list" not in st.session_state:

    st.session_state.Dci_len_list = [0, 0]

if "main_L" not in st.session_state:

    st.session_state.main_L = 1.02

if "main_chimney_width" not in st.session_state:

    st.session_state.main_chimney_width = 1.00

if "main_ap_depth" not in st.session_state:

    st.session_state.main_ap_depth = 0.100

if "main_theta" not in st.session_state:

    st.session_state.main_theta = 45.0

if "main_radiation" not in st.session_state:

    st.session_state.main_radiation = 100.0

for _key, _value in {
    "channel_length": st.session_state.main_L,
    "channel_width": st.session_state.main_chimney_width,
    "channel_depth": st.session_state.main_ap_depth,
    "channel_tilt": st.session_state.main_theta,
}.items():
    if _key not in st.session_state:
        st.session_state[_key] = _value

if "plate_1_flux" not in st.session_state:

    st.session_state.plate_1_flux = st.session_state.main_radiation

if "plate_2_flux" not in st.session_state:

    st.session_state.plate_2_flux = 0.0

def ensure_pipe_state():

    """Keep dynamic pipe lists consistent with their independent 0--3 counts."""

    for count_key, length_key, area_key, connector_key, connector_len_key, default_length in [
        ("n_out", "Lo_list", "Do_list", "Dco_list", "Dco_len_list", 90),
        ("n_in", "Lin_list", "Din_list", "Dci_list", "Dci_len_list", 95),
    ]:
        count = max(0, min(3, int(st.session_state[count_key])))
        st.session_state[count_key] = count
        while len(st.session_state[length_key]) < count:
            st.session_state[length_key].append(default_length)
        while len(st.session_state[area_key]) < count:
            st.session_state[area_key].append(18)
        del st.session_state[length_key][count:]
        del st.session_state[area_key][count:]

        connector_count = max(0, count - 1)
        while len(st.session_state[connector_key]) < connector_count:
            st.session_state[connector_key].append(16)
        while len(st.session_state[connector_len_key]) < connector_count:
            st.session_state[connector_len_key].append(0)
        del st.session_state[connector_key][connector_count:]
        del st.session_state[connector_len_key][connector_count:]

ensure_pipe_state()

# Callbacks

def add_outer_pipe():

    if st.session_state.n_out < 3:
        st.session_state.n_out += 1
        ensure_pipe_state()

def remove_outer_pipe():

    if st.session_state.n_out > 0:
        st.session_state.n_out -= 1
        ensure_pipe_state()

def add_inner_pipe():

    if st.session_state.n_in < 3:
        st.session_state.n_in += 1
        ensure_pipe_state()

def remove_inner_pipe():

    if st.session_state.n_in > 0:
        st.session_state.n_in -= 1
        ensure_pipe_state()

def sync_channel(source_key, state_key, *mirror_keys):

    value = st.session_state[source_key]
    st.session_state[state_key] = value
    for key in mirror_keys:
        if key != source_key:
            st.session_state[key] = value

def sync_pipe_value(source_key, list_key, index, *mirror_keys):

    if index >= len(st.session_state[list_key]):
        return
    value = st.session_state[source_key]
    st.session_state[list_key][index] = value
    for key in mirror_keys:
        if key != source_key:
            st.session_state[key] = value

def ensure_widget_value(key, value):

    if key not in st.session_state:
        st.session_state[key] = value

def sync_main_L():

    sync_channel("sidebar_L_input", "main_L", "channel_length", "chimney_height_input")

def sync_main_width():

    sync_channel("sidebar_width_input", "main_chimney_width", "channel_width", "chimney_w_input")

def sync_main_depth():

    sync_channel("sidebar_depth_input", "main_ap_depth", "channel_depth", "chimney_d_input")

def sync_main_theta():

    sync_channel("sidebar_theta_input", "main_theta", "channel_tilt", "theta_input")

def sync_main_radiation():

    sync_channel("sidebar_rad_input", "main_radiation", "plate_1_flux", "plate_1_input", "rad_input")

def sync_channel_length():

    sync_channel("channel_length", "main_L", "sidebar_L_input", "chimney_height_input")

def sync_channel_width():

    sync_channel("channel_width", "main_chimney_width", "sidebar_width_input", "chimney_w_input")

def sync_channel_depth():

    sync_channel("channel_depth", "main_ap_depth", "sidebar_depth_input", "chimney_d_input")

def sync_channel_tilt():

    sync_channel("channel_tilt", "main_theta", "sidebar_theta_input", "theta_input")

def sync_plate_1():

    sync_channel("plate_1_input", "plate_1_flux", "main_radiation", "sidebar_rad_input", "rad_input")

def sync_advanced_theta():

    sync_channel("theta_input", "main_theta", "channel_tilt", "sidebar_theta_input")

def sync_advanced_depth():

    sync_channel("chimney_d_input", "main_ap_depth", "channel_depth", "sidebar_depth_input")

def sync_advanced_length():

    sync_channel("chimney_height_input", "main_L", "channel_length", "sidebar_L_input")

def sync_advanced_width():

    sync_channel("chimney_w_input", "main_chimney_width", "channel_width", "sidebar_width_input")

def sync_advanced_radiation():

    sync_channel("rad_input", "main_radiation", "plate_1_flux", "plate_1_input", "sidebar_rad_input")

# ====================== Title ======================

st.markdown(

    """

    <h1 style="font-size: 24px; font-weight:bold;">🔥 Solar Chimney Simulation Tool</h1>

    """,

    unsafe_allow_html=True

)

st.markdown("<hr>", unsafe_allow_html=True)

# ====================== Sidebar ======================

n_out = st.session_state.n_out
n_in = st.session_state.n_in
Lo = list(st.session_state.Lo_list)
Do = list(st.session_state.Do_list)
Dco = list(st.session_state.Dco_list)
Lin = list(st.session_state.Lin_list)
Din = list(st.session_state.Din_list)
Dci = list(st.session_state.Dci_list)
D_joint_lower = st.session_state.get("D_joint_lower", 55)
D_joint_upper = st.session_state.get("D_joint_upper", 55)
q = st.session_state.get("calc_q", 0)
Tamb = st.session_state.get("calc_tamb", 20.0)
theta = st.session_state.main_theta
ap_depth = st.session_state.main_ap_depth
heat_flux_input = st.session_state.plate_1_flux
L = st.session_state.main_L
chimney_width = st.session_state.main_chimney_width
inlet_area = st.session_state.get("calc_inlet_area", 0.10)
inlet_loss = st.session_state.get("calc_inlet_loss", 0.5)
entrance_section = st.session_state.get("calc_entrance_section", "No")
room_setting = st.session_state.get("calc_room_setting", "No")
outlet_area = st.session_state.get("calc_outlet_area", 0.10)
outlet_loss = st.session_state.get("calc_outlet_loss", 1.0)
outflow_section = st.session_state.get("calc_outflow_section", "No")
consider_heat_loss = st.session_state.get("calc_consider_heat_loss", "No")
heating_plate_position = st.session_state.get("calc_heating_plate_position", "Vertical")
roughness_height = st.session_state.get("calc_roughness_height", 0.5)
calc_btn = False

# Legacy sidebar controls are kept only as a reference and no longer rendered.
if False:

    st.header("Input Parameters")

    with st.expander("Core Geometry Segments", expanded=True):

        st.subheader("Outer Side Pipe Segments")

        n_out = st.session_state.n_out

        Lo = []

        Do = []

        Dco = []

        for j in range(n_out):

            c1, c2 = st.columns(2)

            with c1:

                ensure_widget_value(f"Lo{j}", st.session_state.Lo_list[j])

                val = st.number_input(
                    f"L_o,{j+1} (mm)", min_value=0, max_value=800,
                    key=f"Lo{j}", on_change=sync_pipe_value,
                    args=(f"Lo{j}", "Lo_list", j, f"outer_len_{j+1}"),
                )

                st.session_state.Lo_list[j] = val

                Lo.append(val)

            with c2:

                ensure_widget_value(f"Do{j}", st.session_state.Do_list[j])

                val = st.number_input(
                    f"D_o,{j+1} (mm)", min_value=0, max_value=80,
                    key=f"Do{j}", on_change=sync_pipe_value,
                    args=(f"Do{j}", "Do_list", j, f"outer_a_{j+1}"),
                )

                st.session_state.Do_list[j] = val

                Do.append(val)

            if j < n_out - 1:

                ensure_widget_value(f"Dco{j}", st.session_state.Dco_list[j])

                val = st.number_input(
                    f"D_co,{j+1} (mm)", min_value=0, max_value=120,
                    key=f"Dco{j}", on_change=sync_pipe_value,
                    args=(f"Dco{j}", "Dco_list", j, f"outer_connector_a_{j+1}"),
                )

                st.session_state.Dco_list[j] = val

                Dco.append(val)

        col_o_add, col_o_del = st.columns(2)

        with col_o_add:

            st.button("Add Outer Pipe", on_click=add_outer_pipe, use_container_width=True,
                      disabled=st.session_state.n_out >= 3)

        with col_o_del:

            st.button("Remove Outer Pipe", on_click=remove_outer_pipe, use_container_width=True,
                      disabled=st.session_state.n_out <= 0)

        st.markdown("---")

        st.subheader("Inner Side Pipe Segments")

        n_in = st.session_state.n_in

        Lin = []

        Din = []

        Dci = []

        for j in range(n_in):

            c1, c2 = st.columns(2)

            with c1:

                ensure_widget_value(f"Lin{j}", st.session_state.Lin_list[j])

                val = st.number_input(
                    f"L_in,{j+1} (mm)", min_value=0, max_value=800,
                    key=f"Lin{j}", on_change=sync_pipe_value,
                    args=(f"Lin{j}", "Lin_list", j, f"inner_len_{j+1}"),
                )

                st.session_state.Lin_list[j] = val

                Lin.append(val)

            with c2:

                ensure_widget_value(f"Din{j}", st.session_state.Din_list[j])

                val = st.number_input(
                    f"D_in,{j+1} (mm)", min_value=0, max_value=80,
                    key=f"Din{j}", on_change=sync_pipe_value,
                    args=(f"Din{j}", "Din_list", j, f"inner_a_{j+1}"),
                )

                st.session_state.Din_list[j] = val

                Din.append(val)

            if j < n_in - 1:

                ensure_widget_value(f"Dci{j}", st.session_state.Dci_list[j])

                val = st.number_input(
                    f"D_ci,{j+1} (mm)", min_value=0, max_value=120,
                    key=f"Dci{j}", on_change=sync_pipe_value,
                    args=(f"Dci{j}", "Dci_list", j, f"inner_connector_a_{j+1}"),
                )

                st.session_state.Dci_list[j] = val

                Dci.append(val)

        col_i_add, col_i_del = st.columns(2)

        with col_i_add:

            st.button("Add Inner Pipe", on_click=add_inner_pipe, use_container_width=True,
                      disabled=st.session_state.n_in >= 3)

        with col_i_del:

            st.button("Remove Inner Pipe", on_click=remove_inner_pipe, use_container_width=True,
                      disabled=st.session_state.n_in <= 0)

        st.markdown("---")

        st.subheader("Joint Connection Diameter")

        c_j1, c_j2 = st.columns(2)

        with c_j1:

            D_joint_lower = st.number_input("Lower Joint Diameter (mm)", value=55, min_value=0, max_value=200)

        with c_j2:

            D_joint_upper = st.number_input("Upper Joint Diameter (mm)", value=55, min_value=0, max_value=200)

    with st.expander("Main Parameters", expanded=True):

        ensure_widget_value("sidebar_L_input", st.session_state.main_L)

        st.number_input("Chimney Height (m)", min_value=0.1, max_value=20.0,

                        key="sidebar_L_input", on_change=sync_main_L, format="%.2f")

        ensure_widget_value("sidebar_width_input", st.session_state.main_chimney_width)

        st.number_input("Chimney Width (m)", min_value=0.1, max_value=10.0,

                        key="sidebar_width_input", on_change=sync_main_width, format="%.2f")

        ensure_widget_value("sidebar_depth_input", st.session_state.main_ap_depth)

        st.number_input("Chimney Depth (m)", min_value=0.01, max_value=5.0,

                        key="sidebar_depth_input", on_change=sync_main_depth, format="%.3f")

        ensure_widget_value("sidebar_theta_input", st.session_state.main_theta)

        st.number_input("Tilt Angle (°)", min_value=0.0, max_value=90.0,

                        key="sidebar_theta_input", on_change=sync_main_theta, format="%.1f")

        ensure_widget_value("sidebar_rad_input", st.session_state.main_radiation)

        st.number_input("Radiation (W/m²)", min_value=0.0, max_value=50000.0,

                        key="sidebar_rad_input", on_change=sync_main_radiation, format="%.1f")

    with st.expander("Parameters", expanded=False):

        st.markdown('<p class="advanced-note">No special working condition? All values below can retain default setting without modification</p>', unsafe_allow_html=True)

        c1_base, c2_base = st.columns(2)

        with c1_base:

            q = st.number_input("Air Pressure (Pa)", value=0, min_value=0, max_value=500000)

            ensure_widget_value("theta_input", st.session_state.main_theta)

            theta = st.number_input(

                "Tilt Angle (°)",

                min_value=0.0,

                max_value=90.0,

                key="theta_input",

                on_change=sync_advanced_theta

            )

            ensure_widget_value("chimney_d_input", st.session_state.main_ap_depth)

            ap_depth = st.number_input(

                "Chimney Depth (m)",

                min_value=0.01,

                max_value=5.00,

                key="chimney_d_input",

                on_change=sync_advanced_depth

            )

            inlet_area = st.number_input("Inlet Area (m²)", value=0.10, min_value=0.01, max_value=100.00, format="%.2f")

            inlet_loss = st.number_input("Inlet Loss Coeff", value=0.5, min_value=0.0, max_value=100.0)

            entrance_section = st.selectbox("Entrance Section", ["No", "Yes"], index=0)

            room_setting = st.selectbox("Room Setting", ["No", "Yes"], index=0)

        with c2_base:

            Tamb = st.number_input("Ambient Temperature (°C)", value=20.0, min_value=-20.0, max_value=100.0)

            ensure_widget_value("rad_input", st.session_state.main_radiation)

            heat_flux_input = st.number_input(

                "Heating Intensity (W/m²)",

                min_value=0.0,

                max_value=50000.0,

                key="rad_input",

                on_change=sync_advanced_radiation

            )

            ensure_widget_value("chimney_height_input", st.session_state.main_L)

            L = st.number_input(

                "Chimney Height (m)",

                min_value=0.1,

                max_value=20.0,

                key="chimney_height_input",

                on_change=sync_advanced_length

            )

            ensure_widget_value("chimney_w_input", st.session_state.main_chimney_width)

            chimney_width = st.number_input(

                "Chimney Width (m)",

                min_value=0.1,

                max_value=10.0,

                key="chimney_w_input",

                on_change=sync_advanced_width

            )

            outlet_area = st.number_input("Outlet Area (m²)", value=0.10, min_value=0.01, max_value=100.00, format="%.2f")

            outlet_loss = st.number_input("Outlet Loss Coeff", value=1.0, min_value=0.0, max_value=100.0)

            outflow_section = st.selectbox("Outflow Section", ["No", "Yes"], index=0)

            consider_heat_loss = st.selectbox("Consider Heat Loss", ["No", "Yes"], index=0)

        heating_plate_position = st.selectbox("Heating Plate Position", ["Vertical", "Horizontal", "Inclined"], index=0)

        roughness_height = st.number_input("Roughness Height of the wall (m)", value=0.5, min_value=0.0, max_value=10.0)

    st.markdown("---")

    calc_btn = st.button("Start Calculation", type="primary", use_container_width=True)

    reset_btn = st.button("Reset Parameters", use_container_width=True)

    st.markdown('<p class="sidebar-small-note">Default Parameters: Air Pressure, Heating Intensity, Ambient Temperature, Gap Depth, other flow & environmental parameters</p>', unsafe_allow_html=True)

    if reset_btn:

        for k in list(st.session_state.keys()):

            if ("Lo" in k or "Do" in k or "Lin" in k or "Din" in k or "Dco" in k or "Dci" in k
                    or "n_out" in k or "n_in" in k or "last_calc" in k or "main_" in k
                    or k.startswith("channel_") or k.startswith("plate_")
                    or k.startswith("outer_") or k.startswith("inner_")):

                del st.session_state[k]

        st.rerun()

# ====================== Calculation ======================

if calc_btn:

    try:

        input_content = f"""{q}

{Tamb}

{n_out}

{heat_flux_input}

{theta}

{L}

{ap_depth}

{chimney_width}

{roughness_height}

{inlet_area}

{outlet_area}

{inlet_loss}

{outlet_loss}

"""

        with open("Solarchimney_INPUT.dat", "w", encoding="utf-8") as f:

            f.write(input_content)

        st.info("✅ Input file Solarchimney_INPUT.dat generated successfully")

        exe_path = "SolarChimney.exe"

        if not os.path.exists(exe_path):

            st.error(f"❌ Cannot find executable file {exe_path}, place it in the same folder as app.py!")

        else:

            subprocess.run([exe_path], shell=True, check=True)

            st.info("✅ Calculation program finished, loading results...")

        if not os.path.exists("result.dat"):

            st.error("❌ result.dat not generated, calculation failed!")

        else:

            with open("result.dat", "r", encoding="utf-8") as f:

                all_lines = f.readlines()

            if len(all_lines) < 2:

                st.error("❌ Insufficient data in result.dat, no calculation output")

            else:

                res_lines = all_lines[1:]

                first_line = res_lines[0].strip()

                num_list = first_line.split(",")

                if len(num_list) < 7:

                    st.error(f"❌ Missing result fields, only {len(num_list)} data loaded")

                else:

                    calc_data = {

                        "q": float(q),

                        "Tamb": float(Tamb),

                        "n_out": int(n_out),

                        "heat_flux_input": float(heat_flux_input),

                        "heating_plate_position": heating_plate_position,

                        "theta": float(theta),

                        "L": float(L),

                        "ap_depth": float(ap_depth),

                        "chimney_width": float(chimney_width),

                        "roughness_height": float(roughness_height),

                        "inlet_area": float(inlet_area),

                        "outlet_area": float(outlet_area),

                        "inlet_loss": float(inlet_loss),

                        "outlet_loss": float(outlet_loss),

                        "entrance_section": entrance_section,

                        "outflow_section": outflow_section,

                        "room_setting": room_setting,

                        "consider_heat_loss": consider_heat_loss,

                        "mass_flowrate": float(num_list[1]),

                        "f_ch": float(num_list[2]),

                        "T_out": float(num_list[3]),

                        "x": float(num_list[4]),

                        "Re_ch": float(num_list[5]),

                        "Ra_p": float(num_list[6])

                    }

                    st.session_state.last_calc = calc_data

                    st.success("Calculation complete, results loaded!")

    except Exception as err:

        st.error(f"Calculation runtime error: {err}")

def run_calculation():

    calc_q = float(st.session_state.get("calc_q", 0))
    calc_tamb = float(st.session_state.get("calc_tamb", 20.0))
    calc_n_out = int(st.session_state.n_out)
    calc_heat_flux = float(st.session_state.plate_1_flux)
    calc_theta = float(st.session_state.main_theta)
    calc_l = float(st.session_state.main_L)
    calc_depth = float(st.session_state.main_ap_depth)
    calc_width = float(st.session_state.main_chimney_width)
    calc_roughness = float(st.session_state.get("calc_roughness_height", 0.5))
    calc_inlet_area = float(st.session_state.get("calc_inlet_area", 0.10))
    calc_outlet_area = float(st.session_state.get("calc_outlet_area", 0.10))
    calc_inlet_loss = float(st.session_state.get("calc_inlet_loss", 0.5))
    calc_outlet_loss = float(st.session_state.get("calc_outlet_loss", 1.0))

    try:

        input_content = f"""{calc_q}

{calc_tamb}

{calc_n_out}

{calc_heat_flux}

{calc_theta}

{calc_l}

{calc_depth}

{calc_width}

{calc_roughness}

{calc_inlet_area}

{calc_outlet_area}

{calc_inlet_loss}

{calc_outlet_loss}

"""

        with open("Solarchimney_INPUT.dat", "w", encoding="utf-8") as f:
            f.write(input_content)

        exe_path = "SolarChimney.exe"
        if not os.path.exists(exe_path):
            st.session_state.calc_status = f"Cannot find executable file {exe_path}."
            st.session_state.calc_status_type = "error"
            return

        subprocess.run([exe_path], shell=True, check=True)

        if not os.path.exists("result.dat"):
            st.session_state.calc_status = "result.dat was not generated; calculation failed."
            st.session_state.calc_status_type = "error"
            return

        with open("result.dat", "r", encoding="utf-8") as f:
            all_lines = f.readlines()

        if len(all_lines) < 2:
            st.session_state.calc_status = "result.dat has insufficient data."
            st.session_state.calc_status_type = "error"
            return

        num_list = all_lines[1].strip().split(",")
        if len(num_list) < 7:
            st.session_state.calc_status = f"Missing result fields; only {len(num_list)} values loaded."
            st.session_state.calc_status_type = "error"
            return

        st.session_state.last_calc = {
            "q": calc_q,
            "Tamb": calc_tamb,
            "n_out": calc_n_out,
            "heat_flux_input": calc_heat_flux,
            "heating_plate_position": st.session_state.get("calc_heating_plate_position", "Vertical"),
            "theta": calc_theta,
            "L": calc_l,
            "ap_depth": calc_depth,
            "chimney_width": calc_width,
            "roughness_height": calc_roughness,
            "inlet_area": calc_inlet_area,
            "outlet_area": calc_outlet_area,
            "inlet_loss": calc_inlet_loss,
            "outlet_loss": calc_outlet_loss,
            "entrance_section": st.session_state.get("calc_entrance_section", "No"),
            "outflow_section": st.session_state.get("calc_outflow_section", "No"),
            "room_setting": st.session_state.get("calc_room_setting", "No"),
            "consider_heat_loss": st.session_state.get("calc_consider_heat_loss", "No"),
            "mass_flowrate": float(num_list[1]),
            "f_ch": float(num_list[2]),
            "T_out": float(num_list[3]),
            "x": float(num_list[4]),
            "Re_ch": float(num_list[5]),
            "Ra_p": float(num_list[6]),
        }
        st.session_state.calc_status = "Calculation complete, results loaded."
        st.session_state.calc_status_type = "success"

    except Exception as err:
        st.session_state.calc_status = f"Calculation runtime error: {err}"
        st.session_state.calc_status_type = "error"


def get_result_text():

    if "last_calc" not in st.session_state:
        return "Click [Run] to run simulation.\nResults will appear here."

    res = st.session_state.last_calc
    return (
        f"Air Pressure: {res['q']:.0f} Pa\n"
        f"Ambient Temp: {res['Tamb']:.1f} °C\n"
        f"Plume Number: {res['n_out']}\n"
        f"Heating Intensity: {res['heat_flux_input']:.1f} W/m²\n"
        f"Tilt Angle: {res['theta']:.1f} °\n"
        f"Chimney Length: {res['L']:.2f} m\n"
        f"Gap Depth: {res['ap_depth']:.3f} m\n"
        f"Chimney Width: {res['chimney_width']:.3f} m\n"
        f"Mass Flow Rate: {res['mass_flowrate']:.4f} kg/s\n"
        f"f_ch: {res['f_ch']:.4f}\n"
        f"T_out: {res['T_out']:.2f} °C\n"
        f"x: {res['x']:.4f}\n"
        f"Re_ch: {res['Re_ch']:.0f}\n"
        f"Ra_p: {res['Ra_p']:.2e}"
    )


# ====================== Geometry Drawing (Matplotlib) ======================

st.subheader("Geometry Model")

fig, ax = plt.subplots(figsize=(14, 5.8), constrained_layout=True)

ax.set_aspect('equal')

ax.axis('off')

ax.set_facecolor("#ffffff")

origin_x, origin_y = 210, 420 + 130

rad = np.radians(st.session_state.main_theta)

L_mm = float(st.session_state.main_L) * 1000

dx = L_mm * np.cos(rad)

dy = L_mm * np.sin(rad)

blue_x = origin_x

blue_y = origin_y

gold_x = blue_x + dx

gold_y = blue_y + dy

total_out_mm = sum(Lo) + sum(Dco)

total_in_mm = sum(Lin) + sum(Dci)

canvas_w_px = 1100

denom = max(1.0, max(total_out_mm, total_in_mm) + dx)

mm2px = 0.7 * canvas_w_px / denom

mm2px = np.clip(mm2px, 0.5, 2.2)

def mm(v):

    return float(v) * mm2px

def draw_element_shadow(ax, x, y_center, width_px, height_px, color="#111827", alpha=0.20, offset_x=5, offset_y=-5, zorder_shadow=2):

    shadow = FancyBboxPatch(

        (x + offset_x, y_center - height_px / 2 + offset_y),

        width_px,

        height_px,

        boxstyle=f"round,pad=0.02,rounding_size={height_px / 2}",

        facecolor=color,

        edgecolor="none",

        alpha=alpha,

        zorder=zorder_shadow

    )

    ax.add_patch(shadow)

def draw_circle_shadow(ax, cx, cy, d_px, color="#111827", alpha=0.20, offset_x=5, offset_y=-5, zorder_shadow=1):

    r = d_px / 2

    shadow = Circle(

        (cx + offset_x, cy + offset_y),

        radius=r,

        facecolor=color,

        edgecolor="none",

        alpha=alpha,

        zorder=zorder_shadow

    )

    ax.add_patch(shadow)

def draw_round_rect(x, y_center, length_px, height_px, color, lw=1.0):

    draw_element_shadow(ax, x, y_center, length_px, height_px)

    rect = FancyBboxPatch(

        (x, y_center - height_px / 2),

        length_px,

        height_px,

        boxstyle=f"round,pad=0.02,rounding_size={height_px / 2}",

        facecolor=color,

        edgecolor="#1f2937",

        linewidth=lw,

        zorder=3

    )

    ax.add_patch(rect)

    return (x, y_center, length_px, height_px)

def draw_circle(cx, cy, d_px, color, lw=1.0):

    draw_circle_shadow(ax, cx, cy, d_px)

    r = d_px / 2

    circle = Circle((cx, cy), radius=r, facecolor=color, edgecolor="#1f2937", linewidth=lw, zorder=2)

    ax.add_patch(circle)

    return r

def add_callout(x0, y0, text, mode="up", idx=0, max_base_len=52, shorten_step=12, fontsize=4.8):

    current_extend = max_base_len - idx * shorten_step

    current_extend = max(current_extend, 18)

    horizontal_extend = 28 - idx * 4

    horizontal_extend = max(horizontal_extend, 10)

    direction = 1 if mode == "up" else -1
    elbow_y = y0 + direction * current_extend
    final_text_x = x0 + horizontal_extend + 30

    # Two separate segments make the leader a true right-angle callout.
    ax.plot([x0, x0], [y0, elbow_y], color='#94a3b8', linewidth=0.35, alpha=0.42, zorder=2.2)
    ax.plot([x0, final_text_x], [elbow_y, elbow_y], color='#94a3b8', linewidth=0.35, alpha=0.42, zorder=2.2)
    ax.text(final_text_x + 3, elbow_y, text, fontsize=fontsize, color='#475569', alpha=0.48, zorder=2.2, ha="left", va="center")
    return final_text_x, elbow_y

# Joint circles

r_blue_joint = draw_circle(blue_x, blue_y, mm(D_joint_lower), color="#2f63b8")

r_gold_joint = draw_circle(gold_x, gold_y, mm(D_joint_upper), color="#c9a15a")

nx = np.sin(rad)

ny = -np.cos(rad)

r_lower_temp = mm(D_joint_lower) / 2

r_upper_temp = mm(D_joint_upper) / 2

p0_x = blue_x + nx * r_lower_temp

p0_y = blue_y + ny * r_lower_temp

p1_x = gold_x + nx * r_upper_temp

p1_y = gold_y + ny * r_upper_temp

p02_x = blue_x - nx * r_lower_temp

p02_y = blue_y - ny * r_lower_temp

p12_x = gold_x - nx * r_upper_temp

p12_y = gold_y - ny * r_upper_temp

ax.plot([p0_x + 3.5, p1_x + 3.5], [p0_y - 5.5, p1_y - 5.5], color="#222222", linewidth=2.6, alpha=0.16, zorder=0)

ax.plot([p02_x + 3.5, p12_x + 3.5], [p02_y - 5.5, p12_y - 5.5], color="#222222", linewidth=2.6, alpha=0.16, zorder=0)

ax.plot([p0_x, p1_x], [p0_y, p1_y], color='#d44b4b', linewidth=2.4, zorder=1)

ax.plot([p02_x, p12_x], [p02_y, p12_y], color='#d44b4b', linewidth=2.4, zorder=1)

mid_x = (blue_x + gold_x) / 2

mid_y = (blue_y + gold_y) / 2

ax.text(mid_x - 12, mid_y - 14, f'L = {st.session_state.main_L:.2f} m', fontsize=10, fontweight='bold', zorder=4)

ax.text(
    mid_x - 12,
    mid_y - 42,
    f"Width = {st.session_state.main_chimney_width:.2f} m   Depth = {st.session_state.main_ap_depth:.3f} m   Tilt = {st.session_state.main_theta:.1f}°",
    fontsize=8.5,
    color="#334155",
    zorder=4,
)

# Inner

inner_center_y = blue_y

inner_start_x = blue_x + r_blue_joint

current_x = inner_start_x

inner_pipe_anchors = []
inner_connector_anchors = []

for j in range(int(n_in)):

    seg_len = mm(Lin[j])

    seg_h = mm(Din[j])

    draw_round_rect(current_x, inner_center_y, seg_len, seg_h, "#2f63b8")

    attach_x = current_x + seg_len * 0.35

    attach_y = inner_center_y

    callout_x, callout_y = add_callout(attach_x, attach_y, f"Inner {j+1}: A / Len", mode="down", idx=j * 2, fontsize=4.8)
    inner_pipe_anchors.append((callout_x, callout_y))

    current_x += seg_len

    if j < int(n_in) - 1:

        connect_r = mm(Dci[j]) / 2

        draw_circle(current_x + connect_r, inner_center_y, mm(Dci[j]), color="#1f4f99")

        callout_x, callout_y = add_callout(current_x + connect_r, inner_center_y, f"Inner joint {j+1}: A / Len", mode="down", idx=j * 2 + 1, fontsize=4.4)
        inner_connector_anchors.append((callout_x, callout_y))

        current_x += connect_r * 2

# Outer

outer_center_y = gold_y

outer_start_x = gold_x + r_gold_joint

current_x = outer_start_x

outer_pipe_anchors = []
outer_connector_anchors = []

for j in range(int(n_out)):

    seg_len = mm(Lo[j])

    seg_h = mm(Do[j])

    draw_round_rect(current_x, outer_center_y, seg_len, seg_h, "#c9a15a")

    attach_x = current_x + seg_len * 0.35

    attach_y = outer_center_y

    callout_x, callout_y = add_callout(attach_x, attach_y, f"Outer {j+1}: A / Len", mode="up", idx=j * 2, fontsize=4.8)
    outer_pipe_anchors.append((callout_x, callout_y))

    current_x += seg_len

    if j < int(n_out) - 1:

        connect_r = mm(Dco[j]) / 2

        draw_circle(current_x + connect_r, outer_center_y, mm(Dco[j]), color="#b88f45")

        callout_x, callout_y = add_callout(current_x + connect_r, outer_center_y, f"Outer joint {j+1}: A / Len", mode="up", idx=j * 2 + 1, fontsize=4.4)
        outer_connector_anchors.append((callout_x, callout_y))

        current_x += connect_r * 2

# Sun

sun_cx, sun_cy = 50, 1350

sun_radius = 32

sun = Circle((sun_cx, sun_cy), sun_radius, facecolor="#FFD93D", edgecolor="#F2A900", linewidth=1.5, zorder=4)

ax.add_patch(sun)

glow = Circle((sun_cx, sun_cy), sun_radius * 1.6, facecolor="#FFE88A", alpha=0.35, edgecolor="none", zorder=3)

ax.add_patch(glow)

for angle in np.linspace(0, 360, 12, endpoint=False):

    rad_angle = np.radians(angle)

    x0 = sun_cx + np.cos(rad_angle) * (sun_radius * 1.05)

    y0 = sun_cy + np.sin(rad_angle) * (sun_radius * 1.05)

    x1 = sun_cx + np.cos(rad_angle) * (sun_radius * 1.6)

    y1 = sun_cy + np.sin(rad_angle) * (sun_radius * 1.6)

    ax.plot([x0, x1], [y0, y1], color="#F2A900", linewidth=2.5, alpha=0.65, zorder=3)

# Result box

if 'last_calc' in st.session_state:

    res = st.session_state.last_calc

    result_text = (

        f"Air Pressure: {res['q']:.0f} Pa\n"

        f"Ambient Temp: {res['Tamb']:.1f} °C\n"

        f"Plume Number: {res['n_out']}\n"

        f"Heating Intensity: {res['heat_flux_input']:.1f} W/m²\n"

        f"Tilt Angle: {res['theta']:.1f} °\n"

        f"Chimney Length: {res['L']:.2f} m\n"

        f"Gap Depth: {res['ap_depth']:.3f} m\n"

        f"Chimney Width: {res['chimney_width']:.3f} m\n"

        f"Mass Flow Rate: {res['mass_flowrate']:.4f} kg/s\n"

        f"f_ch: {res['f_ch']:.4f}\n"

        f"T_out: {res['T_out']:.2f} °C\n"

        f"x: {res['x']:.4f}\n"

        f"Re_ch: {res['Re_ch']:.0f}\n"

        f"Ra_p: {res['Ra_p']:.2e}"

    )

else:

    result_text = (

        "Click [Start Calculation]\n"

        "to run simulation.\n"

        "Results will appear here."

    )

result_box_x, result_box_y = blue_x, blue_y

result_w, result_h = 0, 0

result_shadow = Rectangle(

    (result_box_x + 6, result_box_y - result_h - 6),

    result_w,

    result_h,

    facecolor="#111827",

    edgecolor="none",

    alpha=0.0,

    zorder=4

)

ax.add_patch(result_shadow)

result_bg = Rectangle(

    (result_box_x, result_box_y - result_h),

    result_w,

    result_h,

    facecolor="#FFFFFF",

    edgecolor="none",

    linewidth=0.0,

    alpha=0.0,

    zorder=5

)

ax.add_patch(result_bg)

ax.text(result_box_x + 12, result_box_y - result_h + 15, "Calculation Results", fontsize=11, fontweight="bold", color="#111827", alpha=0.0, zorder=6)

ax.text(result_box_x + 12, result_box_y - result_h + 38, result_text, fontsize=9, color="#374151", alpha=0.0, linespacing=1.8, zorder=6)

ax.autoscale_view()

x_min, x_max = ax.get_xlim()

y_min, y_max = ax.get_ylim()

padding_x = 100

padding_top = 14

padding_bottom = 80

ax.set_xlim(x_min - padding_x, x_max + padding_x)

ax.set_ylim(y_min - padding_bottom, y_max + padding_top)

bb_x0 = x_min - padding_x

bb_y0 = y_min - padding_bottom

bb_w = x_max - x_min + 2 * padding_x

bb_h = y_max - y_min + padding_top + padding_bottom

cut_top = 22

cut_bottom = 18

bb_y0 += cut_bottom + 12

bb_h = bb_h - cut_top - cut_bottom

round_radius = 18

shadow_offset_x = 7

shadow_offset_y = -7

shadow_box = FancyBboxPatch(

    (bb_x0 + shadow_offset_x, bb_y0 + shadow_offset_y),

    bb_w,

    bb_h,

    boxstyle=f"round,pad=0,rounding_size={round_radius}",

    facecolor="#333840",

    edgecolor="none",

    alpha=0.30,

    zorder=-3

)

ax.add_patch(shadow_box)

main_card = FancyBboxPatch(

    (bb_x0, bb_y0),

    bb_w,

    bb_h,

    boxstyle=f"round,pad=0,rounding_size={round_radius}",

    facecolor="#f8f9fa",

    edgecolor="#dee2e6",

    linewidth=1.4,

    zorder=-2

)

ax.add_patch(main_card)

# ====================== Chart Rendering + In-image Controls ======================

# Save before closing the figure; all overlay positions are calculated from the same axes.
buf = io.BytesIO()

fig.savefig(buf, format='png', bbox_inches='tight', dpi=120, facecolor='white')

buf.seek(0)

plot_x0, plot_x1 = ax.get_xlim()
plot_y0, plot_y1 = ax.get_ylim()

def plot_to_percent(x, y, *, min_left=2.0, max_left=92.0, min_top=3.0, max_top=94.0):
    x_pct = 100 * (float(x) - plot_x0) / max(1.0, plot_x1 - plot_x0)
    y_pct = 100 * (plot_y1 - float(y)) / max(1.0, plot_y1 - plot_y0)
    return max(min_left, min(max_left, x_pct)), max(min_top, min(max_top, y_pct))

plt.close(fig)

outer_btn_left, outer_btn_top = plot_to_percent(current_x, outer_center_y, max_left=88.0)
outer_btn_top = max(8.0, outer_btn_top - 4.5)

inner_endpoint_x = inner_start_x
for _j in range(int(n_in)):
    inner_endpoint_x += mm(Lin[_j])
    if _j < int(n_in) - 1:
        inner_endpoint_x += mm(Dci[_j])
inner_btn_left, inner_btn_top = plot_to_percent(inner_endpoint_x, inner_center_y, max_left=88.0)
inner_btn_top = max(54.0, min(78.0, inner_btn_top + 1.0))

overlay_rules = [
    f"""
    div.st-key-pipe_ctrl_outer {{
        position: absolute !important; left: {outer_btn_left:.2f}% !important;
        top: {outer_btn_top:.2f}% !important; width: 76px !important;
        transform: translate(8px, -50%) !important;
        z-index: 140 !important; pointer-events: auto !important;
    }}
    """,
    f"""
    div.st-key-pipe_ctrl_inner {{
        position: absolute !important; left: {inner_btn_left:.2f}% !important;
        top: {inner_btn_top:.2f}% !important; width: 76px !important;
        transform: translate(8px, -50%) !important;
        z-index: 140 !important; pointer-events: auto !important;
    }}
    """,
]

def append_abs_rule(key, left, top, *, width=138, z_index=135):
    overlay_rules.append(
        f"""
        div.st-key-{key} {{
            position: absolute !important; left: {left:.2f}% !important;
            top: {top:.2f}% !important; width: {width}px !important;
            transform: translate(10px, -50%) !important;
            z-index: {z_index} !important; pointer-events: auto !important;
        }}
        """
    )

def stack_geometry_rules(items, *, top_min, top_max, min_gap, left_min, left_max):
    positioned = []
    for key, x, y in items:
        left, top = plot_to_percent(
            x, y,
            min_left=left_min,
            max_left=left_max,
            min_top=top_min,
            max_top=top_max,
        )
        positioned.append({"key": key, "left": left, "top": top})

    if not positioned:
        return

    positioned.sort(key=lambda item: item["top"])
    if len(positioned) == 1:
        append_abs_rule(positioned[0]["key"], positioned[0]["left"], positioned[0]["top"])
        return

    available = top_max - top_min
    gap = min(min_gap, available / max(1, len(positioned) - 1))
    targets = []
    for item in positioned:
        next_top = max(top_min, min(top_max, item["top"]))
        if targets:
            next_top = max(next_top, targets[-1] + gap)
        targets.append(next_top)

    overflow = targets[-1] - top_max
    if overflow > 0:
        targets = [target - overflow for target in targets]
    underflow = top_min - targets[0]
    if underflow > 0:
        targets = [target + underflow for target in targets]
    if targets[-1] > top_max + 0.01:
        targets = [
            top_min + available * idx / max(1, len(positioned) - 1)
            for idx in range(len(positioned))
        ]

    for item, target_top in zip(positioned, targets):
        append_abs_rule(item["key"], item["left"], target_top)

outer_geometry_items = []
for _j, (_x, _y) in enumerate(outer_pipe_anchors):
    outer_geometry_items.append((f"geom_outer_seg_{_j + 1}", _x, _y))
for _j, (_x, _y) in enumerate(outer_connector_anchors):
    outer_geometry_items.append((f"geom_outer_joint_{_j + 1}", _x, _y))

inner_geometry_items = []
for _j, (_x, _y) in enumerate(inner_pipe_anchors):
    inner_geometry_items.append((f"geom_inner_seg_{_j + 1}", _x, _y))
for _j, (_x, _y) in enumerate(inner_connector_anchors):
    inner_geometry_items.append((f"geom_inner_joint_{_j + 1}", _x, _y))

stack_geometry_rules(
    outer_geometry_items,
    top_min=7.0,
    top_max=42.0,
    min_gap=14.0,
    left_min=76.0,
    left_max=84.0,
)
stack_geometry_rules(
    inner_geometry_items,
    top_min=52.0,
    top_max=78.0,
    min_gap=6.5,
    left_min=48.0,
    left_max=65.0,
)

st.markdown(
    f"""
    <style>
    div.st-key-st_image_container {{
        position: relative !important;
    }}
    div.st-key-st_image_container div[data-testid="stImage"] {{
        position: relative !important;
        z-index: 1 !important;
    }}
    div.st-key-channel_config_panel {{
        position: absolute !important; left: 2.5% !important; top: 4.5% !important;
        width: 255px !important; z-index: 118 !important; pointer-events: auto !important;
        background: rgba(255,255,255,0.93) !important;
    }}
    div.st-key-heat_sources_panel {{
        position: absolute !important; left: calc(2.5% + 270px) !important; top: 4.5% !important;
        width: 235px !important; z-index: 118 !important; pointer-events: auto !important;
        background: rgba(255,255,255,0.93) !important;
    }}
    div.st-key-st_image_container div[data-testid="stVerticalBlockBorderWrapper"] {{
        background: rgba(255,255,255,1.0) !important;
        border: 1px solid rgba(148,163,184,0.70) !important;
        border-radius: 8px !important;
        box-shadow: 0 8px 24px rgba(15,23,42,0.12) !important;
    }}
    div.st-key-st_image_container h3 {{
        font-size: 0.86rem !important;
        margin: 0 0 0.25rem 0 !important;
    }}
    div.st-key-st_image_container [data-testid="stCaptionContainer"] p {{
        font-size: 0.72rem !important;
        font-weight: 700 !important;
        color: #111827 !important;
        margin-bottom: 0.15rem !important;
    }}
    div.st-key-st_image_container div[data-testid="stNumberInput"] {{
        margin-bottom: 0.16rem !important;
    }}
    div.st-key-st_image_container div[data-testid="stNumberInput"] label p {{
        font-size: 0.66rem !important;
        line-height: 1.05 !important;
    }}
    div.st-key-st_image_container div[data-testid="stNumberInput"] input {{
        min-height: 1.75rem !important;
        padding: 0.15rem 0.35rem !important;
        font-size: 0.72rem !important;
    }}
    div.st-key-st_image_container div[class*="st-key-geom_"] div[data-testid="stVerticalBlockBorderWrapper"] {{
        padding: 0.22rem !important;
    }}
    div.st-key-st_image_container div[class*="st-key-geom_"] [data-testid="stCaptionContainer"] p {{
        font-size: 0.64rem !important;
        line-height: 1.0 !important;
        margin-bottom: 0.05rem !important;
    }}
    div.st-key-st_image_container div[class*="st-key-geom_"] div[data-testid="stNumberInput"] {{
        margin-bottom: 0 !important;
    }}
    div.st-key-st_image_container div[class*="st-key-geom_"] div[data-testid="stNumberInput"] input {{
        min-height: 1.38rem !important;
        padding: 0.05rem 0.18rem !important;
        font-size: 0.64rem !important;
    }}
    div.st-key-pipe_ctrl_outer div[data-testid="stButton"] button,
    div.st-key-pipe_ctrl_inner div[data-testid="stButton"] button {{
        min-height: 1.65rem !important;
        padding: 0.10rem 0.35rem !important;
        font-size: 0.82rem !important;
        font-weight: 700 !important;
    }}
    {"".join(overlay_rules)}
    </style>
    """,
    unsafe_allow_html=True,
)

ensure_widget_value("plate_1_input", st.session_state.plate_1_flux)
ensure_widget_value("plate_2_input", st.session_state.plate_2_flux)

with st.container(key="st_image_container"):

    st.image(buf, width="stretch", output_format="PNG")

    with st.container(key="pipe_ctrl_outer"):
        outer_add, outer_del = st.columns(2)
        with outer_add:
            st.button("+", key="overlay_outer_add", use_container_width=True, on_click=add_outer_pipe,
                      disabled=st.session_state.n_out >= 3)
        with outer_del:
            st.button("-", key="overlay_outer_del", use_container_width=True, on_click=remove_outer_pipe,
                      disabled=st.session_state.n_out <= 0)

    with st.container(key="pipe_ctrl_inner"):
        inner_add, inner_del = st.columns(2)
        with inner_add:
            st.button("+", key="overlay_inner_add", use_container_width=True, on_click=add_inner_pipe,
                      disabled=st.session_state.n_in >= 3)
        with inner_del:
            st.button("-", key="overlay_inner_del", use_container_width=True, on_click=remove_inner_pipe,
                      disabled=st.session_state.n_in <= 0)

    with st.container(border=True, key="channel_config_panel"):
        st.markdown("### Channel Config")
        st.number_input("Length (m)", min_value=0.1, max_value=20.0, format="%.2f",
                        key="channel_length", on_change=sync_channel_length)
        st.number_input("Width (m)", min_value=0.1, max_value=10.0, format="%.2f",
                        key="channel_width", on_change=sync_channel_width)
        st.number_input("Depth (m)", min_value=0.01, max_value=5.0, format="%.3f",
                        key="channel_depth", on_change=sync_channel_depth)
        st.number_input("Tilt (deg)", min_value=0.0, max_value=90.0, format="%.1f",
                        key="channel_tilt", on_change=sync_channel_tilt)

    with st.container(border=True, key="heat_sources_panel"):
        st.markdown("### Heat Sources")
        st.number_input("Plate 1 (W/m2)", min_value=0.0, max_value=50000.0,
                        format="%.1f", key="plate_1_input", on_change=sync_plate_1)
        st.number_input("Plate 2 (W/m2)", min_value=0.0, max_value=50000.0,
                        format="%.1f", key="plate_2_input",
                        on_change=lambda: st.session_state.update(plate_2_flux=st.session_state.plate_2_input))

    def render_geometry_inputs(panel_key, title, a_key, a_list_key, len_key, len_list_key,
                               index, a_max, a_mirror_key=None, len_mirror_key=None):
        ensure_widget_value(a_key, st.session_state[a_list_key][index])
        ensure_widget_value(len_key, st.session_state[len_list_key][index])
        with st.container(border=True, key=panel_key):
            st.caption(f"{title}  A | Len")
            a_col, len_col = st.columns(2)
            a_args = [a_key, a_list_key, index]
            len_args = [len_key, len_list_key, index]
            if a_mirror_key is not None:
                a_args.append(a_mirror_key)
            if len_mirror_key is not None:
                len_args.append(len_mirror_key)
            with a_col:
                st.number_input("A", min_value=0.0, max_value=a_max, format="%.1f",
                                key=a_key, on_change=sync_pipe_value, args=tuple(a_args),
                                label_visibility="collapsed")
            with len_col:
                st.number_input("Len", min_value=0.0, max_value=800.0, format="%.1f",
                                key=len_key, on_change=sync_pipe_value, args=tuple(len_args),
                                label_visibility="collapsed")

    for j in range(int(n_out)):
        render_geometry_inputs(
            f"geom_outer_seg_{j + 1}", f"Outer {j + 1}",
            f"outer_a_{j + 1}", "Do_list", f"outer_len_{j + 1}", "Lo_list",
            j, 80.0, f"Do{j}", f"Lo{j}",
        )
    for j in range(max(0, int(n_out) - 1)):
        render_geometry_inputs(
            f"geom_outer_joint_{j + 1}", f"Outer joint {j + 1}",
            f"outer_connector_a_{j + 1}", "Dco_list",
            f"outer_connector_len_{j + 1}", "Dco_len_list",
            j, 120.0, f"Dco{j}", None,
        )
    for j in range(int(n_in)):
        render_geometry_inputs(
            f"geom_inner_seg_{j + 1}", f"Inner {j + 1}",
            f"inner_a_{j + 1}", "Din_list", f"inner_len_{j + 1}", "Lin_list",
            j, 80.0, f"Din{j}", f"Lin{j}",
        )
    for j in range(max(0, int(n_in) - 1)):
        render_geometry_inputs(
            f"geom_inner_joint_{j + 1}", f"Inner joint {j + 1}",
            f"inner_connector_a_{j + 1}", "Dci_list",
            f"inner_connector_len_{j + 1}", "Dci_len_list",
            j, 120.0, f"Dci{j}", None,
        )

# ====================== Run / Result ======================

st.markdown(
    """
    <style>
    div.st-key-run_result_panel {
        margin-top: 0.75rem !important;
        padding: 0.7rem 0.85rem !important;
        border: 1px solid rgba(148,163,184,0.55) !important;
        border-radius: 8px !important;
        background: rgba(255,255,255,0.98) !important;
    }
    div.st-key-run_result_panel button[kind="primary"] {
        min-height: 3.25rem !important;
        font-size: 1.15rem !important;
        font-weight: 800 !important;
    }
    div.st-key-run_result_panel textarea {
        min-height: 8.5rem !important;
        font-family: Consolas, "Courier New", monospace !important;
        font-size: 0.86rem !important;
        line-height: 1.35 !important;
        background: #ffffff !important;
        color: #111827 !important;
    }
    </style>
    """,
    unsafe_allow_html=True,
)

with st.container(key="run_result_panel"):
    run_col, result_col = st.columns([1, 6], vertical_alignment="top")
    with run_col:
        st.button("Run", type="primary", use_container_width=True, on_click=run_calculation)
    with result_col:
        st.markdown("**Result**")
        status = st.session_state.get("calc_status")
        status_type = st.session_state.get("calc_status_type")
        if status and status_type == "success":
            st.success(status)
        elif status:
            st.error(status)
        st.text_area(
            "Result output",
            value=get_result_text(),
            height=160,
            disabled=True,
            label_visibility="collapsed",
        )

# ====================== References ======================

st.markdown("---")

st.subheader("References")

ref_text = st.text_area(

    "Reference Materials & Model Basis",

    value="""[1] Fill your literature reference here

[2] Calculation formula & geometric model basis

[3] Experimental data comparison source

""",

    height=180

)

st.caption("You can store research references, model derivation and experiment description in this text box.")

# ====================== Contact Form ======================

st.markdown("<hr style='margin-top:20px;'>")

st.subheader("Contact Us")

st.info("If you need technical support, model customization or detailed documents, please fill in the form below, we will contact you as soon as possible!")

if "contact_name" not in st.session_state:

    st.session_state.contact_name = ""

if "contact_email" not in st.session_state:

    st.session_state.contact_email = ""

if "contact_phone" not in st.session_state:

    st.session_state.contact_phone = ""

if "contact_msg" not in st.session_state:

    st.session_state.contact_msg = ""

c_left, c_right = st.columns(2)

with c_left:

    name_input = st.text_input("Full Name", value=st.session_state.contact_name, placeholder="Please enter your full name")

    phone_input = st.text_input("Phone Number", value=st.session_state.contact_phone, placeholder="Please enter your mobile number")

with c_right:

    email_input = st.text_input("Email Address", value=st.session_state.contact_email, placeholder="Please enter your email address")

msg_input = st.text_area("Requirements / Message", value=st.session_state.contact_msg, height=100, placeholder="Describe your questions, requirements or cooperation intentions...")

submit_contact_btn = st.button("Submit Contact Info", type="primary")

def save_contact(name, phone, email, msg):

    file_name = "contact_records.csv"

    now_time = datetime.now().strftime("%Y-%m-%d %H:%M:%S")

    header = ["Submit Time", "Full Name", "Phone Number", "Email Address", "Message Content"]

    row_data = [now_time, name, phone, email, msg]

    file_exists = os.path.exists(file_name)

    with open(file_name, mode="a", encoding="utf-8-sig", newline="") as f:

        writer = csv.writer(f)

        if not file_exists:

            writer.writerow(header)

        writer.writerow(row_data)

if submit_contact_btn:

    if not name_input.strip():

        st.warning("Please fill in your full name!")

    elif not phone_input.strip() and not email_input.strip():

        st.warning("Please fill at least one of phone number or email address for us to contact you!")

    else:

        save_contact(name_input, phone_input, email_input, msg_input)

        st.success("🎉 Submitted successfully! We have saved your contact information and will reply to you shortly.")

        st.session_state.contact_name = ""

        st.session_state.contact_phone = ""

        st.session_state.contact_email = ""

        st.session_state.contact_msg = ""

        st.rerun()

st.caption("📌 View records locally: Open contact_records.csv in the same folder as this program, all messages are permanently stored")

Youez - 2016 - github.com/yon3zu
LinuXploit