Spaces:
Sleeping
Sleeping
Update pages/1_Simulation.py
Browse files- pages/1_Simulation.py +844 -179
pages/1_Simulation.py
CHANGED
|
@@ -1,74 +1,429 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
|
| 4 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
from datetime import datetime
|
| 6 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
| 7 |
import streamlit as st
|
| 8 |
-
import base64
|
| 9 |
|
| 10 |
-
|
| 11 |
|
| 12 |
# ์ธ์ฆ ์ฒดํฌ
|
| 13 |
if "authenticated" not in st.session_state or not st.session_state["authenticated"]:
|
| 14 |
st.error("โ ์ ๊ทผ ๋ถ๊ฐ: ๋จผ์ ๋ฉ์ธ ํ๋ฉด์์ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ์ธ์.")
|
| 15 |
st.stop()
|
| 16 |
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
#
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
DEFAULT_RESULT = {"THINNING": 0.65, "MAX_FAILURE": 1.02}
|
| 32 |
-
|
| 33 |
DISPLAY_LABELS = ["440", "590", "780"]
|
| 34 |
DISPLAY_TO_MODEL = {"440": "440.0", "590": "590.0", "780": "780.0"}
|
| 35 |
-
|
| 36 |
MATERIAL_THICKNESS_CAP = {"440": 0.17, "590": 0.16, "780": 0.10}
|
| 37 |
|
| 38 |
st.session_state.setdefault("history", [])
|
| 39 |
st.session_state.setdefault("input_mode", "์ง์ ์
๋ ฅ")
|
| 40 |
-
st.session_state.setdefault("material", "590")
|
| 41 |
-
|
| 42 |
-
#
|
| 43 |
-
#
|
| 44 |
-
#
|
| 45 |
-
def
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
def
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
|
| 70 |
-
|
| 71 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 72 |
|
| 73 |
def val_or_range(single_key, range_key, unit=""):
|
| 74 |
mode = st.session_state.get("input_mode", "์ง์ ์
๋ ฅ")
|
|
@@ -79,207 +434,517 @@ def val_or_range(single_key, range_key, unit=""):
|
|
| 79 |
return f"{st.session_state[single_key]}{unit}"
|
| 80 |
return "-"
|
| 81 |
|
| 82 |
-
def
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 86 |
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
tabs = st.tabs(["์กฐ๊ฑด ์ค์ & ์คํ", "๊ฒฐ๊ณผ ์๊ฐํ", "๊ธฐ๋ก ์กฐํ"])
|
| 91 |
|
| 92 |
-
#
|
| 93 |
# 1) ์กฐ๊ฑด ์ค์ & ์คํ
|
| 94 |
-
#
|
| 95 |
with tabs[0]:
|
| 96 |
st.header("์กฐ๊ฑด ์ค์ ")
|
| 97 |
st.markdown("---")
|
| 98 |
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 103 |
-
)
|
| 104 |
-
|
| 105 |
-
cur = st.session_state.get("material", "590")
|
| 106 |
-
idx = DISPLAY_LABELS.index(cur) if cur in DISPLAY_LABELS else 1
|
| 107 |
-
st.session_state["material"] = st.selectbox("์ฌ์ง", DISPLAY_LABELS, index=idx)
|
| 108 |
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 112 |
|
| 113 |
-
# ---------------- ์ง์ ์
๋ ฅ ----------------
|
| 114 |
if st.session_state["input_mode"] == "์ง์ ์
๋ ฅ":
|
|
|
|
|
|
|
|
|
|
| 115 |
col_t, col_d = st.columns(2)
|
| 116 |
with col_t:
|
| 117 |
-
st.session_state["thickness"] = st.selectbox("์์ฌ ๋๊ป (mm)",
|
| 118 |
-
[0.7,0.8,0.9,1.0,1.1,1.2], index=2)
|
| 119 |
with col_d:
|
| 120 |
st.session_state["diameter"] = st.number_input("์ง๊ฒฝ (mm)", 10, 1000, 20)
|
| 121 |
|
| 122 |
col1, col2 = st.columns(2)
|
| 123 |
with col1:
|
| 124 |
-
st.session_state["upperR"] = st.number_input("์๋จ R", 1
|
| 125 |
with col2:
|
| 126 |
-
st.session_state["lowerR"] = st.number_input("ํ๋จ R", 1
|
| 127 |
|
| 128 |
st.session_state["degree"] = st.number_input("๊ฐ๋ (ยฐ)", 0, 90, 75)
|
| 129 |
|
| 130 |
-
if st.button("์๋ฎฌ๋ ์ด์
์คํํ๊ธฐ", type="primary"
|
| 131 |
-
|
| 132 |
-
|
| 133 |
-
st.session_state["
|
| 134 |
-
st.session_state["
|
| 135 |
-
|
| 136 |
-
|
| 137 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 138 |
st.session_state["beadType"],
|
| 139 |
)
|
| 140 |
try:
|
| 141 |
-
|
|
|
|
|
|
|
| 142 |
except Exception as e:
|
| 143 |
-
st.error(f"๋ชจ๋ธ ์์ธก ์คํจ: {e}")
|
| 144 |
-
|
| 145 |
-
th = calc_thinning(df.loc[0,"thickness"], df.loc[0,"upper_radius"], df.loc[0,"lower_radius"])
|
| 146 |
st.session_state.sim_result = {"THINNING": th, "MAX_FAILURE": mf}
|
| 147 |
-
st.
|
|
|
|
| 148 |
|
| 149 |
-
# ---------------- ๋ฒ์ ๊ฐ ์
๋ ฅ ----------------
|
| 150 |
else:
|
| 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 |
try:
|
| 188 |
-
|
|
|
|
| 189 |
except Exception as e:
|
| 190 |
-
st.error(f"๋ชจ๋ธ ์์ธก ์คํจ: {e}")
|
| 191 |
-
st.stop()
|
| 192 |
|
| 193 |
-
df_all["THINNING"]
|
| 194 |
-
calc_thinning(r["thickness"], r["upper_radius"], r["lower_radius"])
|
| 195 |
-
for _, r in df_all.iterrows()
|
| 196 |
-
]
|
| 197 |
df_all["MAX_FAILURE"] = mf_pred
|
| 198 |
|
| 199 |
-
st.session_state.
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 206 |
# 2) ๊ฒฐ๊ณผ ์๊ฐํ
|
| 207 |
-
#
|
| 208 |
with tabs[1]:
|
| 209 |
st.header("๊ฒฐ๊ณผ ์๊ฐํ")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 210 |
result_data = st.session_state.get("sim_result", DEFAULT_RESULT)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
st.
|
| 215 |
-
|
| 216 |
-
st.
|
| 217 |
|
| 218 |
-
|
| 219 |
-
#
|
| 220 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 221 |
with tabs[2]:
|
| 222 |
st.header("๊ธฐ๋ก ์กฐํ")
|
| 223 |
|
| 224 |
-
col1, col2, col3
|
| 225 |
with col1:
|
| 226 |
save_btn = st.button("ํ์ฌ ๊ฒฐ๊ณผ ์ ์ฅ", type="primary", use_container_width=True)
|
| 227 |
with col2:
|
| 228 |
select_all_btn = st.button("์ ์ฒด ์ ํ", use_container_width=True)
|
| 229 |
with col3:
|
| 230 |
delete_btn = st.button("์ ํ ํญ๋ชฉ ์ญ์ ", use_container_width=True)
|
| 231 |
-
with col4:
|
| 232 |
-
export_btn = st.button("CSV ๋ด๋ณด๋ด๊ธฐ", use_container_width=True)
|
| 233 |
|
|
|
|
| 234 |
if save_btn:
|
| 235 |
if "sim_result" not in st.session_state:
|
| 236 |
st.warning("๋จผ์ ์๋ฎฌ๋ ์ด์
์ ์คํํ์ธ์.")
|
| 237 |
else:
|
| 238 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 239 |
"์ ํ": False,
|
| 240 |
-
"Index":
|
| 241 |
"์ ์ฅ์๊ฐ": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 242 |
-
"๋น๋ ํ์
":
|
| 243 |
-
"์
๋ ฅ ๋ฐฉ์":
|
| 244 |
"์์ฌ ๋๊ป (mm)": val_or_range("thickness", "thicknessRange", " mm"),
|
| 245 |
-
"์ฌ์ง":
|
| 246 |
"์ง๊ฒฝ": val_or_range("diameter", "diameterRange", " mm"),
|
| 247 |
"๊ฐ๋": val_or_range("degree", "degreeRange", "ยฐ"),
|
| 248 |
"์๋จ R": val_or_range("upperR", "upperRRange"),
|
| 249 |
"ํ๋จ R": val_or_range("lowerR", "lowerRRange"),
|
| 250 |
-
"THINNING": st.session_state.sim_result.get("THINNING"),
|
| 251 |
-
"MAX_FAILURE": st.session_state.sim_result.get("MAX_FAILURE"),
|
|
|
|
|
|
|
|
|
|
| 252 |
}
|
| 253 |
-
st.session_state.history.append(new_entry)
|
| 254 |
-
st.success("ํ์ฌ ๊ฒฐ๊ณผ๊ฐ ๊ธฐ๋ก์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 255 |
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
st.download_button("CSV ๋ค์ด๋ก๋", csv, file_name="simulation_history.csv", mime="text/csv")
|
| 261 |
-
else:
|
| 262 |
-
st.info("๋ด๋ณด๋ผ ๊ธฐ๋ก์ด ์์ต๋๋ค.")
|
| 263 |
|
|
|
|
| 264 |
if st.session_state.history:
|
| 265 |
df = pd.DataFrame(st.session_state.history)
|
| 266 |
cols = ["์ ํ"] + [c for c in df.columns if c != "์ ํ"]
|
| 267 |
df = df[cols]
|
| 268 |
|
| 269 |
if select_all_btn:
|
| 270 |
-
for
|
| 271 |
-
|
| 272 |
-
|
| 273 |
|
| 274 |
st.subheader(f"๊ธฐ๋ก ํ
์ด๋ธ (์ด {len(df)}๊ฑด, ์ฒดํฌ ํ ์ญ์ ๊ฐ๋ฅ)")
|
| 275 |
-
edited_df = st.data_editor(
|
|
|
|
|
|
|
| 276 |
|
| 277 |
if delete_btn:
|
| 278 |
selected_index = edited_df[edited_df["์ ํ"] == True]["Index"].tolist()
|
| 279 |
st.session_state.history = [
|
| 280 |
-
rec for rec in st.session_state.history if rec
|
| 281 |
]
|
| 282 |
st.success(f"{len(selected_index)}๊ฐ ํญ๋ชฉ ์ญ์ ์๋ฃ!")
|
| 283 |
st.rerun()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 284 |
else:
|
| 285 |
-
st.info("์์ง ์ ์ฅ๋ ๊ธฐ๋ก์ด ์์ต๋๋ค.")
|
|
|
|
| 1 |
+
st.title("์๋ฎฌ๋ ์ด์
์คํ")
|
| 2 |
+
# app_simulation_multi.py
|
| 3 |
+
# - ์ง์ ์
๋ ฅ: ๋จ์ผ ์ฌ์ง/๋น๋
|
| 4 |
+
# - ๋ฒ์ ์
๋ ฅ: ๋ค์ค ์ฌ์ง/๋ค์ค ๋น๋ โ ์ ์ฒด ๊ฒฝ์ฐ ์ ์์ฑ ํ ๊ท์น/์ค์ ํํฐ โ MAX_FAILURE & THINNING ๋์ ์์ธก(Blend)
|
| 5 |
+
from dashboard_theme.theme import inject
|
| 6 |
+
inject("graphite_gold")
|
| 7 |
+
|
| 8 |
+
import os
|
| 9 |
+
import itertools
|
| 10 |
+
import warnings
|
| 11 |
+
from pathlib import Path
|
| 12 |
from datetime import datetime
|
| 13 |
+
from decimal import Decimal
|
| 14 |
+
from typing import Dict, List, Tuple, Union
|
| 15 |
+
|
| 16 |
+
import numpy as np
|
| 17 |
+
import pandas as pd
|
| 18 |
import streamlit as st
|
|
|
|
| 19 |
|
| 20 |
+
warnings.filterwarnings("ignore", category=FutureWarning)
|
| 21 |
|
| 22 |
# ์ธ์ฆ ์ฒดํฌ
|
| 23 |
if "authenticated" not in st.session_state or not st.session_state["authenticated"]:
|
| 24 |
st.error("โ ์ ๊ทผ ๋ถ๊ฐ: ๋จผ์ ๋ฉ์ธ ํ๋ฉด์์ ๋น๋ฐ๋ฒํธ๋ฅผ ์
๋ ฅํ์ธ์.")
|
| 25 |
st.stop()
|
| 26 |
|
| 27 |
+
# =========================================
|
| 28 |
+
# ํ์ด์ง ์ค์ & ์คํ์ผ
|
| 29 |
+
# =========================================
|
| 30 |
+
|
| 31 |
+
def _set_env_from_secrets(key: str):
|
| 32 |
+
try:
|
| 33 |
+
val = st.secrets[key]
|
| 34 |
+
except Exception:
|
| 35 |
+
val = None
|
| 36 |
+
if val:
|
| 37 |
+
os.environ[key] = str(val)
|
| 38 |
+
|
| 39 |
+
_set_env_from_secrets("FS_THIN_ART_DIR")
|
| 40 |
+
_set_env_from_secrets("FS_MF_ART_DIR")
|
| 41 |
+
|
| 42 |
+
# ---- Compact ๋ชจ๋: ์ ์ฒด ์ฌ๋ฐฑ/ํจ๋ฉ ์ถ์ ----
|
| 43 |
+
st.markdown("""
|
| 44 |
+
<style>
|
| 45 |
+
/* ์ ์ฒด ์ปจํ
์ด๋ ์ํ ํจ๋ฉ ์ค์ด๊ธฐ */
|
| 46 |
+
.block-container{padding-top:0.6rem !important; padding-bottom:1.25rem !important;}
|
| 47 |
+
/* ์ ๋ชฉ/์์ ๋ชฉ ๊ฐ๊ฒฉ */
|
| 48 |
+
h1, h2, h3{margin-top:0.4rem !important; margin-bottom:0.6rem !important;}
|
| 49 |
+
/* ํจ๋๊ณผ metric ์นด๋ ๊ฐ๊ฒฉ/๋์ด ์กฐ๊ธ ์ถ์ */
|
| 50 |
+
.panel{margin:8px 0 12px !important; padding:16px 16px 12px !important;}
|
| 51 |
+
.metric{min-height:128px !important; padding:18px !important;}
|
| 52 |
+
.metric .value{margin:4px 0 6px !important;}
|
| 53 |
+
/* ๊ตฌ๋ถ์ ๊ฐ๊ฒฉ */
|
| 54 |
+
hr, .stDivider{margin:10px 0 !important;}
|
| 55 |
+
/* ์น์
์ฌ์ด ๋ง์ง ์กฐ๊ธ์ฉ ์ค์ด๊ธฐ */
|
| 56 |
+
.stMarkdown, [data-testid="stMarkdownContainer"]{margin:0 !important;}
|
| 57 |
+
/* ์บก์
-์ต์คํฌ๋ ํ์ดํธ ๋ฌถ๊ธฐ */
|
| 58 |
+
.tight-block .stCaption, .tight-block small{margin-top:0 !important; display:block;}
|
| 59 |
+
.tight-block [data-testid="stExpander"] > details{margin-top:6px !important;}
|
| 60 |
+
</style>
|
| 61 |
+
""", unsafe_allow_html=True)
|
| 62 |
+
|
| 63 |
+
# =========================================
|
| 64 |
+
# ๊ธฐ๋ณธ๊ฐ & ์ ์ญ ์ํ
|
| 65 |
+
# =========================================
|
| 66 |
DEFAULT_RESULT = {"THINNING": 0.65, "MAX_FAILURE": 1.02}
|
|
|
|
| 67 |
DISPLAY_LABELS = ["440", "590", "780"]
|
| 68 |
DISPLAY_TO_MODEL = {"440": "440.0", "590": "590.0", "780": "780.0"}
|
|
|
|
| 69 |
MATERIAL_THICKNESS_CAP = {"440": 0.17, "590": 0.16, "780": 0.10}
|
| 70 |
|
| 71 |
st.session_state.setdefault("history", [])
|
| 72 |
st.session_state.setdefault("input_mode", "์ง์ ์
๋ ฅ")
|
| 73 |
+
st.session_state.setdefault("material", "590") # ์ง์ ์
๋ ฅ ๊ธฐ๋ณธ๊ฐ
|
| 74 |
+
|
| 75 |
+
# =========================================
|
| 76 |
+
# ํ์ผ ํ์
|
| 77 |
+
# =========================================
|
| 78 |
+
def _find_file(name: str):
|
| 79 |
+
here = Path(__file__).parent
|
| 80 |
+
for p in [here / name, here / "assets" / name, Path.cwd() / name, Path("/mnt/data") / name]:
|
| 81 |
+
if p.exists():
|
| 82 |
+
return str(p)
|
| 83 |
+
return ""
|
| 84 |
+
|
| 85 |
+
# ํ์ ์ ์ ๋๊ฒฝ๋ก๋ก ๋ฐ๊ฟ๋ ๋จ
|
| 86 |
+
RULES_XLSX = _find_file("ํ์๋ณ_ํ์ฉ๋ฒ์์ ๋ฆฌํ.xlsx")
|
| 87 |
+
SWEEP_XLSX = _find_file("์ง๊ฒฝ๋ณ_์ค๊ณ๋ณ๊ฒฝํ์ฉ๋ฒ์.xlsx")
|
| 88 |
+
|
| 89 |
+
# =========================================
|
| 90 |
+
# ์กฐํฉ ์์ฑ & ํํฐ ์ ํธ
|
| 91 |
+
# =========================================
|
| 92 |
+
def dseq(start: float, stop: float, step: float, q="0.001") -> List[float]:
|
| 93 |
+
s, e, stp = map(lambda x: Decimal(str(x)), [start, stop, step])
|
| 94 |
+
vals, cur = [], s
|
| 95 |
+
while cur <= e + Decimal("1e-12"):
|
| 96 |
+
vals.append(float(cur.quantize(Decimal(q))))
|
| 97 |
+
cur += stp
|
| 98 |
+
return vals
|
| 99 |
+
|
| 100 |
+
def bead_to_lr(bead_value: Union[str, None]) -> Tuple[int, int]:
|
| 101 |
+
mapping = {None:(2,2),"none":(0,0),"right":(0,1),"left":(1,0),"double":(1,1)}
|
| 102 |
+
key = bead_value.lower() if isinstance(bead_value, str) else bead_value
|
| 103 |
+
return mapping.get(key, (0, 0))
|
| 104 |
+
|
| 105 |
+
def make_all_combinations(cfg: Dict) -> pd.DataFrame:
|
| 106 |
+
bead_values = cfg.get("beads") or [None]
|
| 107 |
+
bead_info = [(b, *bead_to_lr(b)) for b in bead_values]
|
| 108 |
+
|
| 109 |
+
materials = cfg["materials"]
|
| 110 |
+
thickness = dseq(cfg["min_thickness"], cfg["max_thickness"], cfg["thickness_step"])
|
| 111 |
+
diameter = [int(x) for x in dseq(cfg["min_diameter"], cfg["max_diameter"], cfg["diameter_step"], q="1")]
|
| 112 |
+
upper_r = dseq(cfg["upper_min"], cfg["upper_max"], cfg["upper_step"])
|
| 113 |
+
lower_r = dseq(cfg["lower_min"], cfg["lower_max"], cfg["lower_step"])
|
| 114 |
+
degree = [int(x) for x in dseq(cfg["min_degree"], cfg["max_degree"], cfg["degree_step"], q="1")]
|
| 115 |
+
|
| 116 |
+
grid = itertools.product(materials, thickness, upper_r, lower_r, diameter, degree, bead_info)
|
| 117 |
+
rows = []
|
| 118 |
+
for mat, th, ur, lr, dia, deg, bead_t in grid:
|
| 119 |
+
bead_name, lb, rb = bead_t
|
| 120 |
+
rows.append((mat, th, ur, lr, dia, deg, bead_name, lb, rb))
|
| 121 |
+
|
| 122 |
+
df = pd.DataFrame(
|
| 123 |
+
rows,
|
| 124 |
+
columns=["material", "thickness", "upper_radius", "lower_radius",
|
| 125 |
+
"diameter", "degree", "bead", "LB", "RB"]
|
| 126 |
+
)
|
| 127 |
+
return df
|
| 128 |
+
|
| 129 |
+
# ----- Sweep ํ๊ณ -----
|
| 130 |
+
def build_limit_dicts(df_sweep: pd.DataFrame):
|
| 131 |
+
t = df_sweep.copy().replace("F", np.nan)
|
| 132 |
+
if "Sweep" in t.columns:
|
| 133 |
+
t = t.set_index("Sweep")
|
| 134 |
+
new_cols = []
|
| 135 |
+
for c in t.columns:
|
| 136 |
+
try: new_cols.append(int(c))
|
| 137 |
+
except: new_cols.append(c)
|
| 138 |
+
t.columns = new_cols
|
| 139 |
+
long = t.stack(dropna=False).reset_index()
|
| 140 |
+
long.columns = ["row", "degree", "limit"]
|
| 141 |
+
tmp = long["row"].str.extract(r"(?P<diameter>\d+)_(?P<which>upper|lower)_radius")
|
| 142 |
+
long = pd.concat([long, tmp], axis=1)
|
| 143 |
+
long["diameter"] = pd.to_numeric(long["diameter"], errors="coerce")
|
| 144 |
+
long["degree"] = pd.to_numeric(long["degree"], errors="coerce")
|
| 145 |
+
long["limit"] = pd.to_numeric(long["limit"], errors="coerce")
|
| 146 |
+
upper = long[long["which"]=="upper"].dropna(subset=["diameter","degree"])
|
| 147 |
+
lower = long[long["which"]=="lower"].dropna(subset=["diameter","degree"])
|
| 148 |
+
upper_dict = {(int(d), int(g)): v for d, g, v in zip(upper["diameter"], upper["degree"], upper["limit"]) }
|
| 149 |
+
lower_dict = {(int(d), int(g)): v for d, g, v in zip(lower["diameter"], lower["degree"], lower["limit"]) }
|
| 150 |
+
return upper_dict, lower_dict
|
| 151 |
+
|
| 152 |
+
def filter_grid_by_sweep_limits(df_grid: pd.DataFrame, df_sweep: pd.DataFrame) -> pd.DataFrame:
|
| 153 |
+
upper_dict, lower_dict = build_limit_dicts(df_sweep)
|
| 154 |
+
key = list(zip(df_grid["diameter"].astype(int), df_grid["degree"].astype(int)))
|
| 155 |
+
df_grid = df_grid.copy()
|
| 156 |
+
df_grid["limit_upper"] = [upper_dict.get(k, np.nan) for k in key]
|
| 157 |
+
df_grid["limit_lower"] = [lower_dict.get(k, np.nan) for k in key]
|
| 158 |
+
not_nan = df_grid["limit_upper"].notna() & df_grid["limit_lower"].notna()
|
| 159 |
+
within = (df_grid["upper_radius"] <= df_grid["limit_upper"]) & \
|
| 160 |
+
(df_grid["lower_radius"] <= df_grid["limit_lower"])
|
| 161 |
+
return df_grid[not_nan & within].reset_index(drop=True)
|
| 162 |
+
|
| 163 |
+
# ----- ๊ท์น ์ํธ -----
|
| 164 |
+
SHEET_BY_BEAD = {"left":"left", "right":"right", "double":"both", "none":"none"}
|
| 165 |
+
|
| 166 |
+
def _normalize_input_df(df: pd.DataFrame) -> pd.DataFrame:
|
| 167 |
+
df2 = df.copy()
|
| 168 |
+
df2.columns = [str(c).strip() for c in df2.columns]
|
| 169 |
+
rename = {}
|
| 170 |
+
for c in df2.columns:
|
| 171 |
+
lc = c.lower().strip()
|
| 172 |
+
if lc == "diamater": rename[c] = "diameter"
|
| 173 |
+
elif lc in ("material","diameter","degree","bead"): rename[c] = lc
|
| 174 |
+
df2 = df2.rename(columns=rename)
|
| 175 |
+
need = {"material","diameter","degree","bead"}
|
| 176 |
+
missing = need - set(df2.columns)
|
| 177 |
+
if missing:
|
| 178 |
+
raise ValueError(f"์
๋ ฅ ๋ฐ์ดํฐํ๋ ์์ ํ์ํ ์ปฌ๋ผ์ด ์์ต๋๋ค: {missing}")
|
| 179 |
+
df2["bead"] = df2["bead"].astype(str).str.strip().str.lower()
|
| 180 |
+
for c in ["material","diameter","degree"]:
|
| 181 |
+
df2[c] = pd.to_numeric(df2[c], errors="coerce")
|
| 182 |
+
return df2.dropna(subset=["material","diameter","degree"]).copy()
|
| 183 |
+
|
| 184 |
+
def _read_rule_sheet(xlsx_path: str, sheet_name: str) -> pd.DataFrame:
|
| 185 |
+
rule = pd.read_excel(xlsx_path, sheet_name=sheet_name)
|
| 186 |
+
rule.columns = rule.columns.str.strip().str.lower()
|
| 187 |
+
rule = rule.rename(columns={"diamater":"diameter"})
|
| 188 |
+
need = {"material","diameter","min_degree","max_degree"}
|
| 189 |
+
missing = need - set(rule.columns)
|
| 190 |
+
if missing:
|
| 191 |
+
raise ValueError(f"๊ท์น ์ํธ '{sheet_name}'์ ํ์ํ ์ปฌ๋ผ์ด ์์ต๋๋ค: {missing}")
|
| 192 |
+
for c in need: rule[c] = pd.to_numeric(rule[c], errors="coerce")
|
| 193 |
+
rule = rule.dropna(subset=list(need)).copy()
|
| 194 |
+
rule = rule.astype({"material":"int64","diameter":"int64"})
|
| 195 |
+
return rule[["material","diameter","min_degree","max_degree"]]
|
| 196 |
+
|
| 197 |
+
def _apply_rules(df_part: pd.DataFrame, rule: pd.DataFrame) -> pd.DataFrame:
|
| 198 |
+
if df_part.empty: return df_part.copy()
|
| 199 |
+
df_part = df_part[df_part["material"].isin(rule["material"].unique())].copy()
|
| 200 |
+
if df_part.empty: return df_part
|
| 201 |
+
merged = df_part.merge(rule, on=["material","diameter"], how="left")
|
| 202 |
+
mask = (
|
| 203 |
+
merged["min_degree"].notna()
|
| 204 |
+
& merged["max_degree"].notna()
|
| 205 |
+
& (merged["degree"] >= merged["min_degree"])
|
| 206 |
+
& (merged["degree"] <= merged["max_degree"])
|
| 207 |
+
)
|
| 208 |
+
return merged.loc[mask, df_part.columns].reset_index(drop=True)
|
| 209 |
+
|
| 210 |
+
def filter_all_by_bead(df: pd.DataFrame, rules_xlsx: str) -> pd.DataFrame:
|
| 211 |
+
base = _normalize_input_df(df)
|
| 212 |
+
outs = []
|
| 213 |
+
for bead_value, sheet in SHEET_BY_BEAD.items():
|
| 214 |
+
part = base[base["bead"] == bead_value].copy()
|
| 215 |
+
if part.empty: continue
|
| 216 |
+
rule = _read_rule_sheet(rules_xlsx, sheet)
|
| 217 |
+
outs.append(_apply_rules(part, rule))
|
| 218 |
+
if not outs: return base.iloc[0:0].copy()
|
| 219 |
+
return pd.concat(outs, axis=0, ignore_index=True).reset_index(drop=True)
|
| 220 |
+
|
| 221 |
+
# =========================================
|
| 222 |
+
# ์์ธก๊ธฐ (๋ ํ๊น ๋์)
|
| 223 |
+
# =========================================
|
| 224 |
+
import torch
|
| 225 |
+
import torch.nn as nn
|
| 226 |
+
import lightgbm as lgb
|
| 227 |
+
|
| 228 |
+
ART_DIR_MF_DEFAULT = "artifacts_blend"
|
| 229 |
+
ART_DIR_THIN_DEFAULT = "artifacts_blend_thinning"
|
| 230 |
+
CAT_COL_DEFAULT = "material"
|
| 231 |
+
NUM_COLS_DEFAULT = ["thickness","diameter","degree","upper_radius","lower_radius","LB","RB"]
|
| 232 |
+
|
| 233 |
+
class FTTransformer(nn.Module):
|
| 234 |
+
def __init__(self, n_materials:int, n_num:int, d_model:int=192, nhead:int=8,
|
| 235 |
+
num_layers:int=4, dim_ff:int=768, dropout:float=0.15):
|
| 236 |
+
super().__init__()
|
| 237 |
+
self.mat_emb = nn.Embedding(n_materials, d_model)
|
| 238 |
+
self.num_linears = nn.ModuleList([nn.Linear(1, d_model) for _ in range(n_num)])
|
| 239 |
+
self.cls = nn.Parameter(torch.zeros(1, 1, d_model))
|
| 240 |
+
nn.init.trunc_normal_(self.cls, std=0.02)
|
| 241 |
+
enc_layer = nn.TransformerEncoderLayer(
|
| 242 |
+
d_model=d_model, nhead=nhead, dim_feedforward=dim_ff,
|
| 243 |
+
dropout=dropout, batch_first=True, activation='gelu', norm_first=True
|
| 244 |
+
)
|
| 245 |
+
self.encoder = nn.TransformerEncoder(enc_layer, num_layers=num_layers)
|
| 246 |
+
self.head = nn.Sequential(nn.LayerNorm(d_model), nn.Linear(d_model, d_model), nn.GELU(), nn.Dropout(dropout), nn.Linear(d_model, 1))
|
| 247 |
+
def forward(self, mat_ids, x_num):
|
| 248 |
+
B = x_num.size(0)
|
| 249 |
+
mat_tok = self.mat_emb(mat_ids).unsqueeze(1)
|
| 250 |
+
num_tok = torch.cat([lin(x_num[:, i:i+1]).unsqueeze(1) for i, lin in enumerate(self.num_linears)], dim=1)
|
| 251 |
+
tokens = torch.cat([self.cls.expand(B, -1, -1), mat_tok, num_tok], dim=1)
|
| 252 |
+
h = self.encoder(tokens)
|
| 253 |
+
return self.head(h[:, 0, :])
|
| 254 |
+
|
| 255 |
+
def _scale_like_fold(X_num: np.ndarray, mean: np.ndarray, scale: np.ndarray) -> np.ndarray:
|
| 256 |
+
return ((X_num - mean) / scale).astype(np.float32)
|
| 257 |
+
|
| 258 |
+
def _canonize_list(materials): return [str(m).strip() for m in materials]
|
| 259 |
+
def _build_alias2canon(canon_list):
|
| 260 |
+
alias2canon = {}
|
| 261 |
+
for c in canon_list:
|
| 262 |
+
alias2canon[c] = c
|
| 263 |
+
s = c.strip(); alias2canon[s] = c
|
| 264 |
+
if "." in s: alias2canon[s.rstrip("0").rstrip(".")] = c
|
| 265 |
+
try:
|
| 266 |
+
v = float(s); alias2canon[str(v)] = c
|
| 267 |
+
if v.is_integer(): alias2canon[str(int(v))] = c
|
| 268 |
+
except: pass
|
| 269 |
+
return alias2canon
|
| 270 |
+
def _first_existing(*paths):
|
| 271 |
+
for p in paths:
|
| 272 |
+
if os.path.exists(p): return p
|
| 273 |
+
return None
|
| 274 |
+
def _load_json_like(art_dir: str, basename: str) -> dict:
|
| 275 |
+
p1 = os.path.join(art_dir, f"{basename}.json"); p2 = os.path.join(art_dir, basename)
|
| 276 |
+
p = _first_existing(p1, p2)
|
| 277 |
+
if p is None: raise FileNotFoundError(f"Missing {basename}(.json) in {self.art_dir}")
|
| 278 |
+
import json; return json.load(open(p, "r", encoding="utf-8"))
|
| 279 |
+
def _load_columns_meta(art_dir: str):
|
| 280 |
+
p = _first_existing(os.path.join(art_dir, "columns_thinning.json"), os.path.join(art_dir, "columns.json"))
|
| 281 |
+
if not p: return None
|
| 282 |
+
import json; return json.load(open(p, "r", encoding="utf-8"))
|
| 283 |
+
|
| 284 |
+
class _SingleTargetBlendPredictor:
|
| 285 |
+
def __init__(self, art_dir:str, lgbm_prefix:str, ftt_prefix:str, alpha_json:str,
|
| 286 |
+
cat_col_default:str=CAT_COL_DEFAULT, num_cols_default:List[str]=None,
|
| 287 |
+
allow_columns_meta:bool=False, unknown_policy:str="error"):
|
| 288 |
+
self.art_dir = art_dir; self.lgbm_prefix=lgbm_prefix; self.ftt_prefix=ftt_prefix
|
| 289 |
+
self.alpha_json=alpha_json; self.unknown_policy=unknown_policy
|
| 290 |
+
self.cat_col = cat_col_default; self.num_cols = list(num_cols_default or NUM_COLS_DEFAULT)
|
| 291 |
+
if allow_columns_meta:
|
| 292 |
+
meta = _load_columns_meta(art_dir)
|
| 293 |
+
if meta: self.cat_col = meta.get("cat_col", self.cat_col); self.num_cols = meta.get("num_cols", self.num_cols)
|
| 294 |
+
self.folds_ft = self._load_ft_folds()
|
| 295 |
+
self.boosters = self._load_lgbm_folds()
|
| 296 |
+
self.materials = self._load_materials()
|
| 297 |
+
self.best_alpha = float(_load_json_like(art_dir, self.alpha_json)["best_alpha"])
|
| 298 |
+
self.materials_canon = _canonize_list(self.materials)
|
| 299 |
+
self.alias2canon = _build_alias2canon(self.materials_canon)
|
| 300 |
+
self.mat2id = {m:i for i,m in enumerate(self.materials_canon)}
|
| 301 |
+
|
| 302 |
+
def _load_ft_folds(self):
|
| 303 |
+
folds=[]
|
| 304 |
+
for fold in range(1,11):
|
| 305 |
+
p = os.path.join(self.art_dir, f"{self.ftt_prefix}{fold}.pt")
|
| 306 |
+
if not os.path.exists(p):
|
| 307 |
+
if folds: break
|
| 308 |
+
continue
|
| 309 |
+
ckpt = torch.load(p, map_location="cpu", weights_only=False)
|
| 310 |
+
model = FTTransformer(len(ckpt["materials"]), len(ckpt["num_cols"]))
|
| 311 |
+
model.load_state_dict(ckpt["state_dict"]); model.eval()
|
| 312 |
+
folds.append({"model":model,"materials":ckpt["materials"],"num_cols":ckpt["num_cols"],
|
| 313 |
+
"scaler_mean":np.array(ckpt["scaler_mean"],dtype=np.float32),
|
| 314 |
+
"scaler_scale":np.array(ckpt["scaler_scale"],dtype=np.float32)})
|
| 315 |
+
if not folds: raise FileNotFoundError(f"No FT checkpoints found in {self.art_dir} (prefix={self.ftt_prefix})")
|
| 316 |
+
return folds
|
| 317 |
+
def _load_lgbm_folds(self):
|
| 318 |
+
boosters=[]
|
| 319 |
+
for fold in range(1,11):
|
| 320 |
+
p = _first_existing(os.path.join(self.art_dir,f"{self.lgbm_prefix}{fold}.txt"),
|
| 321 |
+
os.path.join(self.art_dir,f"{self.lgbm_prefix}{fold}"))
|
| 322 |
+
if p is None:
|
| 323 |
+
if boosters: break
|
| 324 |
+
continue
|
| 325 |
+
boosters.append(lgb.Booster(model_file=p))
|
| 326 |
+
if not boosters: raise FileNotFoundError(f"No LightGBM model files found in {self.art_dir} (prefix={self.lgbm_prefix})")
|
| 327 |
+
return boosters
|
| 328 |
+
def _load_materials(self):
|
| 329 |
+
try: return _load_json_like(self.art_dir,"materials")["materials"]
|
| 330 |
+
except FileNotFoundError: return self.folds_ft[0]["materials"]
|
| 331 |
+
def _prep_df(self, df_new: pd.DataFrame) -> pd.DataFrame:
|
| 332 |
+
df = df_new.copy()
|
| 333 |
+
need = [self.cat_col] + self.num_cols
|
| 334 |
+
missing = [c for c in need if c not in df.columns]
|
| 335 |
+
if missing: raise ValueError(f"Missing columns in input: {missing}")
|
| 336 |
+
df[self.cat_col] = df[self.cat_col].astype(str).str.strip()
|
| 337 |
+
df["_mat_canon"] = df[self.cat_col].map(self.alias2canon)
|
| 338 |
+
if self.unknown_policy == "error":
|
| 339 |
+
unknown = df.loc[df["_mat_canon"].isna(), self.cat_col].unique().tolist()
|
| 340 |
+
if unknown: raise ValueError(f"Unknown materials in input {unknown}. Known materials: {self.materials_canon[:10]}{' ...' if len(self.materials_canon)>10 else ''}")
|
| 341 |
+
df["_mat_id"] = df["_mat_canon"].map(self.mat2id).astype(int)
|
| 342 |
+
else:
|
| 343 |
+
df["_mat_canon"] = df["_mat_canon"].fillna(self.materials_canon[0])
|
| 344 |
+
df["_mat_id"] = df["_mat_canon"].map(self.mat2id).astype(int)
|
| 345 |
+
df[self.num_cols] = df[self.num_cols].apply(pd.to_numeric, errors="coerce")
|
| 346 |
+
if df[self.num_cols].isnull().any().any():
|
| 347 |
+
bad = df[self.num_cols].columns[df[self.num_cols].isnull().any()].tolist()
|
| 348 |
+
raise ValueError(f"Non-numeric values detected in columns: {bad}")
|
| 349 |
+
return df
|
| 350 |
+
def predict_ft(self, df_new: pd.DataFrame) -> np.ndarray:
|
| 351 |
+
df = self._prep_df(df_new); mids = torch.tensor(df["_mat_id"].values, dtype=torch.long)
|
| 352 |
+
preds=[]
|
| 353 |
+
for f in self.folds_ft:
|
| 354 |
+
Xn = df[f["num_cols"]].values.astype(np.float32)
|
| 355 |
+
x_scaled = _scale_like_fold(Xn, f["scaler_mean"], f["scaler_scale"])
|
| 356 |
+
with torch.no_grad():
|
| 357 |
+
p = f["model"](mids, torch.tensor(x_scaled, dtype=torch.float32)).cpu().numpy().ravel()
|
| 358 |
+
preds.append(p)
|
| 359 |
+
return np.mean(preds, axis=0)
|
| 360 |
+
def predict_lgbm(self, df_new: pd.DataFrame) -> np.ndarray:
|
| 361 |
+
df = self._prep_df(df_new)
|
| 362 |
+
X = df[[self.cat_col] + self.num_cols].copy()
|
| 363 |
+
X[self.cat_col] = pd.Categorical(df["_mat_canon"], categories=self.materials_canon)
|
| 364 |
+
preds = [bst.predict(X, num_iteration=getattr(bst,"best_iteration",None)) for bst in self.boosters]
|
| 365 |
+
return np.mean(preds, axis=0)
|
| 366 |
+
def predict_blend(self, df_new: pd.DataFrame, alpha: float|None=None) -> np.ndarray:
|
| 367 |
+
alpha = self.best_alpha if alpha is None else alpha
|
| 368 |
+
return alpha * self.predict_ft(df_new) + (1 - alpha) * self.predict_lgbm(df_new)
|
| 369 |
+
|
| 370 |
+
class MultiTargetBlendPredictor:
|
| 371 |
+
def __init__(self, art_dir_mf:str, art_dir_thin:str, unknown_policy:str="error"):
|
| 372 |
+
self.mf = _SingleTargetBlendPredictor(art_dir=art_dir_mf, lgbm_prefix="lgbm_fold", ftt_prefix="ftt_fold",
|
| 373 |
+
alpha_json="blend_alpha", allow_columns_meta=False,
|
| 374 |
+
unknown_policy=unknown_policy)
|
| 375 |
+
self.thin = _SingleTargetBlendPredictor(art_dir=art_dir_thin, lgbm_prefix="lgbm_thinning_fold",
|
| 376 |
+
ftt_prefix="ftt_thinning_fold", alpha_json="blend_alpha_thinning",
|
| 377 |
+
allow_columns_meta=True, unknown_policy=unknown_policy)
|
| 378 |
+
def predict_both(self, df_new: pd.DataFrame, alpha_mf: float|None=None, alpha_th: float|None=None):
|
| 379 |
+
return {
|
| 380 |
+
"blend_max_failure": self.mf.predict_blend(df_new, alpha_mf),
|
| 381 |
+
"blend_thinning": self.thin.predict_blend(df_new, alpha_th),
|
| 382 |
+
"lgbm_max_failure": self.mf.predict_blend(df_new, 0.0),
|
| 383 |
+
"dl_max_failure": self.mf.predict_blend(df_new, 1.0),
|
| 384 |
+
"lgbm_thinning": self.thin.predict_blend(df_new, 0.0),
|
| 385 |
+
"dl_thinning": self.thin.predict_blend(df_new, 1.0),
|
| 386 |
+
}
|
| 387 |
+
|
| 388 |
+
@st.cache_resource(show_spinner=False)
|
| 389 |
+
def get_predictor():
|
| 390 |
+
art_mf = os.environ.get("FS_MF_ART_DIR", ART_DIR_MF_DEFAULT)
|
| 391 |
+
art_th = os.environ.get("FS_THIN_ART_DIR", ART_DIR_THIN_DEFAULT)
|
| 392 |
+
return MultiTargetBlendPredictor(art_dir_mf=art_mf, art_dir_thin=art_th, unknown_policy="fallback0")
|
| 393 |
+
|
| 394 |
+
def predict_both_blend(df: pd.DataFrame):
|
| 395 |
+
out = get_predictor().predict_both(df)
|
| 396 |
+
return out["blend_max_failure"], out["blend_thinning"]
|
| 397 |
+
|
| 398 |
+
# =========================================
|
| 399 |
+
# UI ์ ํธ
|
| 400 |
+
# =========================================
|
| 401 |
+
def bead_to_flags_ui(bead: str):
|
| 402 |
+
if bead == "Left Bead": return 1,0
|
| 403 |
+
if bead == "Right Bead": return 0,1
|
| 404 |
+
if bead == "Double Bead": return 1,1
|
| 405 |
+
return 0,0
|
| 406 |
+
|
| 407 |
+
def _bead_key_from_label(label: str) -> str:
|
| 408 |
+
return {"No Bead":"none","Left Bead":"left","Right Bead":"right","Double Bead":"double"}[label]
|
| 409 |
+
|
| 410 |
+
# (์
๊ทธ๋ ์ด๋) ์์ด์ฝ ์ง์ Metric ์นด๋
|
| 411 |
+
def metric_card(label: str, value: float, lo: float, hi: float, icon: str = "๐"):
|
| 412 |
+
ok = lo <= float(value) <= hi
|
| 413 |
+
cls = "ok" if ok else "bad"
|
| 414 |
+
status_text = "์ ์" if ok else "๋ฒ์ ๋ฐ"
|
| 415 |
+
st.markdown(
|
| 416 |
+
f"""
|
| 417 |
+
<div class="metric {cls}" style="text-align:center;">
|
| 418 |
+
<div class="label" style="font-weight:600; margin-bottom:6px;">{icon} {label}</div>
|
| 419 |
+
<div class="value" style="font-size:2rem; font-weight:bold;">{float(value):.3f}</div>
|
| 420 |
+
<div class="chip" style="margin-top:4px; margin-bottom:4px;">{status_text}</div>
|
| 421 |
+
<div class="range" style="font-size:0.85rem; color:gray;">ํ์ฉ๋ฒ์: {lo:.2f} ~ {hi:.2f}</div>
|
| 422 |
+
</div>
|
| 423 |
+
""",
|
| 424 |
+
unsafe_allow_html=True
|
| 425 |
+
)
|
| 426 |
+
|
| 427 |
|
| 428 |
def val_or_range(single_key, range_key, unit=""):
|
| 429 |
mode = st.session_state.get("input_mode", "์ง์ ์
๋ ฅ")
|
|
|
|
| 434 |
return f"{st.session_state[single_key]}{unit}"
|
| 435 |
return "-"
|
| 436 |
|
| 437 |
+
def render_cap_table():
|
| 438 |
+
with st.expander("์ํ ๊ธฐ์คํ ๋ณด๊ธฐ", expanded=False):
|
| 439 |
+
st.markdown("""
|
| 440 |
+
<table style="width:100%; border-collapse: collapse;" border="1">
|
| 441 |
+
<tr><th>์ฌ์ง(ํ๊ธฐ)</th><th>๋ชจ๋ธ ๋ผ๋ฒจ</th><th>๋๊ป ๊ฐ์์จ(ํ์ฉ ์ํ)</th></tr>
|
| 442 |
+
<tr><td>440</td><td>440.0</td><td style="color:red">0.17</td></tr>
|
| 443 |
+
<tr><td>590</td><td>590.0</td><td style="color:red">0.16</td></tr>
|
| 444 |
+
<tr><td>780</td><td>780.0</td><td style="color:red">0.10</td></tr>
|
| 445 |
+
</table>
|
| 446 |
+
""", unsafe_allow_html=True)
|
| 447 |
+
|
| 448 |
+
def build_df_single(material_display, thickness, diameter, degree, upperR, lowerR, beadType):
|
| 449 |
+
lb, rb = bead_to_flags_ui(beadType)
|
| 450 |
+
mat_model = DISPLAY_TO_MODEL[material_display]
|
| 451 |
+
return pd.DataFrame([{
|
| 452 |
+
"material": mat_model,
|
| 453 |
+
"thickness": float(thickness),
|
| 454 |
+
"diameter": int(diameter),
|
| 455 |
+
"degree": int(degree),
|
| 456 |
+
"upper_radius": float(upperR),
|
| 457 |
+
"lower_radius": float(lowerR),
|
| 458 |
+
"LB": int(lb), "RB": int(rb),
|
| 459 |
+
}])
|
| 460 |
|
| 461 |
+
@st.cache_resource(show_spinner=False)
|
| 462 |
+
def get_sweep_df():
|
| 463 |
+
return pd.read_excel(SWEEP_XLSX, sheet_name=0) if SWEEP_XLSX else None
|
| 464 |
+
|
| 465 |
+
@st.cache_resource(show_spinner=False)
|
| 466 |
+
def get_sweep_dicts():
|
| 467 |
+
df = get_sweep_df()
|
| 468 |
+
if df is None: return {}, {}
|
| 469 |
+
return build_limit_dicts(df)
|
| 470 |
+
|
| 471 |
+
def validate_direct_input(material, diameter, degree, bead_label, upperR, lowerR):
|
| 472 |
+
if RULES_XLSX:
|
| 473 |
+
bead_key = _bead_key_from_label(bead_label)
|
| 474 |
+
rule = _read_rule_sheet(RULES_XLSX, SHEET_BY_BEAD[bead_key])
|
| 475 |
+
row = rule[(rule["material"] == int(material)) & (rule["diameter"] == int(diameter))]
|
| 476 |
+
if not row.empty:
|
| 477 |
+
r = row.iloc[0]; mn, mx = int(r["min_degree"]), int(r["max_degree"])
|
| 478 |
+
if not (mn <= int(degree) <= mx):
|
| 479 |
+
return False, f"๊ฐ๋ {degree}ยฐ๋ ๊ท์น ๋ฒ์({mn}~{mx}ยฐ) ๋ฐ์
๋๋ค."
|
| 480 |
+
else:
|
| 481 |
+
return True, None
|
| 482 |
+
upper_dict, lower_dict = get_sweep_dicts()
|
| 483 |
+
key = (int(diameter), int(degree))
|
| 484 |
+
u = upper_dict.get(key, np.nan); l = lower_dict.get(key, np.nan)
|
| 485 |
+
if np.isnan(u) or np.isnan(l): return False, "์ค์ํ์ ์๋ ์ง๊ฒฝ/๊ฐ๋ ์กฐํฉ์
๋๋ค."
|
| 486 |
+
if float(upperR) > float(u) or float(lowerR) > float(l):
|
| 487 |
+
return False, f"R ํ๊ณ ์ด๊ณผ: ์๋จR โค {u}, ํ๋จR โค {l} ์ด์ด์ผ ํฉ๋๋ค."
|
| 488 |
+
return True, None
|
| 489 |
+
|
| 490 |
+
def _reset_optimum_summary():
|
| 491 |
+
for k in ["best_filter_thin","best_filter_mf","best_all_thin","best_all_mf"]:
|
| 492 |
+
st.session_state.pop(k, None)
|
| 493 |
+
st.session_state.pop("topcard_source", None)
|
| 494 |
+
|
| 495 |
+
# =========================================
|
| 496 |
+
# ํญ UI
|
| 497 |
+
# =========================================
|
| 498 |
tabs = st.tabs(["์กฐ๊ฑด ์ค์ & ์คํ", "๊ฒฐ๊ณผ ์๊ฐํ", "๊ธฐ๋ก ์กฐํ"])
|
| 499 |
|
| 500 |
+
# -----------------------------------------
|
| 501 |
# 1) ์กฐ๊ฑด ์ค์ & ์คํ
|
| 502 |
+
# -----------------------------------------
|
| 503 |
with tabs[0]:
|
| 504 |
st.header("์กฐ๊ฑด ์ค์ ")
|
| 505 |
st.markdown("---")
|
| 506 |
|
| 507 |
+
st.subheader("์
๋ ฅ ๋ชจ๋")
|
| 508 |
+
st.session_state["input_mode"] = st.radio("์
๋ ฅ ๋ฐฉ์", ["์ง์ ์
๋ ฅ", "๋ฒ์ ๊ฐ ์
๋ ฅ"], horizontal=True, label_visibility="collapsed", key="mode_radio")
|
| 509 |
+
_prev_mode = st.session_state.get("_prev_input_mode")
|
| 510 |
+
if _prev_mode is not None and _prev_mode != st.session_state["input_mode"]:
|
| 511 |
+
_reset_optimum_summary()
|
| 512 |
+
st.session_state["_prev_input_mode"] = st.session_state["input_mode"]
|
|
|
|
|
|
|
|
|
|
| 513 |
|
| 514 |
+
col_b1, col_b2 = st.columns(2)
|
| 515 |
+
if st.session_state["input_mode"] == "์ง์ ์
๋ ฅ":
|
| 516 |
+
with col_b1:
|
| 517 |
+
st.session_state["beadType"] = st.selectbox("๋น๋ ํ์
์ ํ", ["No Bead","Double Bead","Left Bead","Right Bead"])
|
| 518 |
+
with col_b2:
|
| 519 |
+
cur = st.session_state.get("material", "590")
|
| 520 |
+
idx = DISPLAY_LABELS.index(cur) if cur in DISPLAY_LABELS else 1
|
| 521 |
+
st.session_state["material"] = st.selectbox("์ฌ์ง", DISPLAY_LABELS, index=idx)
|
| 522 |
+
else:
|
| 523 |
+
with col_b1:
|
| 524 |
+
st.session_state["beadTypes_multi"] = st.multiselect(
|
| 525 |
+
"๋น๋ ํ์
์ ํ (๋ณต์ ๊ฐ๋ฅ)", ["No Bead","Double Bead","Left Bead","Right Bead"],
|
| 526 |
+
default=["Right Bead"])
|
| 527 |
+
with col_b2:
|
| 528 |
+
default_materials = [st.session_state.get("material","590")]
|
| 529 |
+
st.session_state["materials_multi"] = st.multiselect(
|
| 530 |
+
"์ฌ์ง (๋ณต์ ๊ฐ๋ฅ)", DISPLAY_LABELS, default=default_materials)
|
| 531 |
+
|
| 532 |
+
st.subheader("์ฑํ ์กฐ๊ฑด")
|
| 533 |
+
st.markdown("<div style='height:6px'></div>", unsafe_allow_html=True)
|
| 534 |
|
|
|
|
| 535 |
if st.session_state["input_mode"] == "์ง์ ์
๋ ฅ":
|
| 536 |
+
for key in ["diameterRange","degreeRange","upperRRange","lowerRRange","thicknessRange"]:
|
| 537 |
+
st.session_state.pop(key, None)
|
| 538 |
+
|
| 539 |
col_t, col_d = st.columns(2)
|
| 540 |
with col_t:
|
| 541 |
+
st.session_state["thickness"] = st.selectbox("์์ฌ ๋๊ป (mm)", [0.7,0.8,0.9,1.0,1.1,1.2], index=2)
|
|
|
|
| 542 |
with col_d:
|
| 543 |
st.session_state["diameter"] = st.number_input("์ง๊ฒฝ (mm)", 10, 1000, 20)
|
| 544 |
|
| 545 |
col1, col2 = st.columns(2)
|
| 546 |
with col1:
|
| 547 |
+
st.session_state["upperR"] = st.number_input("์๋จ R", 1, 100, 4)
|
| 548 |
with col2:
|
| 549 |
+
st.session_state["lowerR"] = st.number_input("ํ๋จ R", 1, 100, 3)
|
| 550 |
|
| 551 |
st.session_state["degree"] = st.number_input("๊ฐ๋ (ยฐ)", 0, 90, 75)
|
| 552 |
|
| 553 |
+
if st.button("์๋ฎฌ๋ ์ด์
์คํํ๊ธฐ", use_container_width=True, type="primary"):
|
| 554 |
+
_reset_optimum_summary()
|
| 555 |
+
ok, err = validate_direct_input(
|
| 556 |
+
st.session_state["material"], st.session_state["diameter"], st.session_state["degree"],
|
| 557 |
+
st.session_state["beadType"], st.session_state["upperR"], st.session_state["lowerR"]
|
| 558 |
+
)
|
| 559 |
+
if not ok:
|
| 560 |
+
st.error(f"๊ท์น ์๋ฐ: {err}")
|
| 561 |
+
st.stop()
|
| 562 |
+
|
| 563 |
+
df = build_df_single(
|
| 564 |
+
st.session_state["material"], st.session_state["thickness"], st.session_state["diameter"],
|
| 565 |
+
st.session_state["degree"], st.session_state["upperR"], st.session_state["lowerR"],
|
| 566 |
st.session_state["beadType"],
|
| 567 |
)
|
| 568 |
try:
|
| 569 |
+
with st.spinner("๋ชจ๋ธ ์์ธก ์ค์
๋๋คโฆ"):
|
| 570 |
+
mf_arr, th_arr = predict_both_blend(df)
|
| 571 |
+
mf = float(mf_arr[0]); th = float(th_arr[0])
|
| 572 |
except Exception as e:
|
| 573 |
+
st.error(f"๋ชจ๋ธ ์์ธก ์คํจ: {e}"); st.stop()
|
| 574 |
+
|
|
|
|
| 575 |
st.session_state.sim_result = {"THINNING": th, "MAX_FAILURE": mf}
|
| 576 |
+
st.session_state.topcard_source = "single"
|
| 577 |
+
st.success("โ
์๋ฎฌ๋ ์ด์
์๋ฃ! (Blend ๋ชจ๋ธ ์ฌ์ฉ)")
|
| 578 |
|
|
|
|
| 579 |
else:
|
| 580 |
+
for key in ["diameter","degree","upperR","lowerR","thickness"]:
|
| 581 |
+
st.session_state.pop(key, None)
|
| 582 |
+
|
| 583 |
+
col_t, col_d = st.columns(2)
|
| 584 |
+
with col_t:
|
| 585 |
+
st.session_state["thicknessRange"] = st.slider("์์ฌ ๋๊ป ๋ฒ์ (mm)", 0.7, 1.2, (0.7, 1.0), step=0.1)
|
| 586 |
+
with col_d:
|
| 587 |
+
st.session_state["diameterRange"] = st.slider("์ง๊ฒฝ ๋ฒ์ (mm)", 10, 50, (15, 30))
|
| 588 |
+
|
| 589 |
+
col1, col2 = st.columns(2)
|
| 590 |
+
with col1:
|
| 591 |
+
st.session_state["upperRRange"] = st.slider("์๋จ R ๋ฒ์", 1, 15, (3, 7))
|
| 592 |
+
with col2:
|
| 593 |
+
st.session_state["lowerRRange"] = st.slider("ํ๋จ R ๋ฒ์", 1, 10, (2, 4))
|
| 594 |
+
|
| 595 |
+
st.session_state["degreeRange"] = st.slider("๊ฐ๋ ๋ฒ์ (ยฐ)", 60, 90, (72, 87))
|
| 596 |
+
|
| 597 |
+
st.divider()
|
| 598 |
+
st.subheader("๊ฒฐ๊ณผ ํํฐ ์กฐ๊ฑด (์ ํ)")
|
| 599 |
+
st.session_state["apply_post_filter"] = st.checkbox("๊ฒฐ๊ณผ ํํฐ ์ ์ฉ (THINNING โค, MAX FAILURE โค)", value=False)
|
| 600 |
+
|
| 601 |
+
selected_mats = st.session_state.get("materials_multi", []) or DISPLAY_LABELS
|
| 602 |
+
caps = [MATERIAL_THICKNESS_CAP[m] for m in selected_mats]
|
| 603 |
+
default_thin_cap = float(min(caps)) if len(caps) else 0.16
|
| 604 |
+
|
| 605 |
+
f2, f3 = st.columns([1,1])
|
| 606 |
+
with f2:
|
| 607 |
+
st.session_state.setdefault("filter_thinning_max", default_thin_cap)
|
| 608 |
+
st.session_state["filter_thinning_max"] = st.number_input(
|
| 609 |
+
"THINNING โค", 0.0, 1.0, float(st.session_state["filter_thinning_max"]),
|
| 610 |
+
step=0.01, format="%.2f", disabled=not st.session_state["apply_post_filter"]
|
| 611 |
+
)
|
| 612 |
+
with f3:
|
| 613 |
+
st.session_state.setdefault("filter_max_failure_max", 1.0)
|
| 614 |
+
st.session_state["filter_max_failure_max"] = st.number_input(
|
| 615 |
+
"MAX FAILURE โค", 0.0, 2.0, float(st.session_state["filter_max_failure_max"]),
|
| 616 |
+
step=0.01, format="%.2f", disabled=not st.session_state["apply_post_filter"]
|
| 617 |
+
)
|
| 618 |
+
|
| 619 |
+
if st.button("์๋ฎฌ๋ ์ด์
์คํํ๊ธฐ", use_container_width=True, type="primary"):
|
| 620 |
+
_reset_optimum_summary()
|
| 621 |
+
|
| 622 |
+
bead_keys = [_bead_key_from_label(b) for b in (st.session_state.get("beadTypes_multi") or ["Right Bead"])]
|
| 623 |
+
mats_disp = st.session_state.get("materials_multi") or DISPLAY_LABELS
|
| 624 |
+
mats_int = [int(m) for m in mats_disp]
|
| 625 |
+
|
| 626 |
+
cfg = {
|
| 627 |
+
"materials": mats_int,
|
| 628 |
+
"min_thickness": st.session_state["thicknessRange"][0],
|
| 629 |
+
"max_thickness": st.session_state["thicknessRange"][1],
|
| 630 |
+
"thickness_step": 0.1,
|
| 631 |
+
"min_diameter": st.session_state["diameterRange"][0],
|
| 632 |
+
"max_diameter": st.session_state["diameterRange"][1],
|
| 633 |
+
"diameter_step": 1,
|
| 634 |
+
"upper_min": st.session_state["upperRRange"][0],
|
| 635 |
+
"upper_max": st.session_state["upperRRange"][1],
|
| 636 |
+
"upper_step": 1.0,
|
| 637 |
+
"lower_min": st.session_state["lowerRRange"][0],
|
| 638 |
+
"lower_max": st.session_state["lowerRRange"][1],
|
| 639 |
+
"lower_step": 1.0,
|
| 640 |
+
"min_degree": st.session_state["degreeRange"][0],
|
| 641 |
+
"max_degree": st.session_state["degreeRange"][1],
|
| 642 |
+
"degree_step": 1,
|
| 643 |
+
"beads": bead_keys,
|
| 644 |
+
}
|
| 645 |
+
df_all = make_all_combinations(cfg)
|
| 646 |
+
|
| 647 |
+
if RULES_XLSX:
|
| 648 |
+
df_all = filter_all_by_bead(df_all, RULES_XLSX)
|
| 649 |
+
else:
|
| 650 |
+
st.warning("๊ท์น ํ์ผ์ ์ฐพ์ง ๋ชปํ์ต๋๋ค. (ํ์๋ณ_ํ์ฉ๋ฒ์์ ๋ฆฌํ.xlsx)")
|
| 651 |
+
sweep_df = get_sweep_df()
|
| 652 |
+
if sweep_df is not None and not df_all.empty:
|
| 653 |
+
df_all = filter_grid_by_sweep_limits(df_all, sweep_df)
|
| 654 |
+
elif sweep_df is None:
|
| 655 |
+
st.warning("์ค์ ํ๊ณ ํ์ผ์ ์ฐพ์ง ๋ชปํ์ต๋๋ค. (์ง๊ฒฝ๋ณ_์ค๊ณ๋ณ๊ฒฝํ์ฉ๋ฒ์.xlsx)")
|
| 656 |
+
|
| 657 |
+
if df_all.empty:
|
| 658 |
+
st.warning("๊ท์น/์ค์ ํ๊ณ ์ ์ฉ ํ ๋จ๋ ์กฐํฉ์ด ์์ต๋๋ค.")
|
| 659 |
+
st.stop()
|
| 660 |
+
|
| 661 |
+
pred_df = df_all.copy()
|
| 662 |
+
pred_df["material"] = pred_df["material"].astype(int).astype(str).map(DISPLAY_TO_MODEL)
|
| 663 |
+
|
| 664 |
try:
|
| 665 |
+
with st.spinner("๋ชจ๋ธ ์์ธก ์ค์
๋๋คโฆ"):
|
| 666 |
+
mf_pred, th_pred = predict_both_blend(pred_df)
|
| 667 |
except Exception as e:
|
| 668 |
+
st.error(f"๋ชจ๋ธ ์์ธก ์คํจ: {e}"); st.stop()
|
|
|
|
| 669 |
|
| 670 |
+
df_all["THINNING"] = th_pred
|
|
|
|
|
|
|
|
|
|
| 671 |
df_all["MAX_FAILURE"] = mf_pred
|
| 672 |
|
| 673 |
+
if st.session_state.get("apply_post_filter", False):
|
| 674 |
+
thin_thr = float(st.session_state["filter_thinning_max"])
|
| 675 |
+
mf_thr = float(st.session_state["filter_max_failure_max"])
|
| 676 |
+
ok_mask = (df_all["THINNING"] <= thin_thr) & (df_all["MAX_FAILURE"] <= mf_thr)
|
| 677 |
+
matched = df_all.loc[ok_mask].copy()
|
| 678 |
+
else:
|
| 679 |
+
matched = df_all.copy()
|
| 680 |
+
|
| 681 |
+
total = len(df_all)
|
| 682 |
+
|
| 683 |
+
def _best_rows(df_ok: pd.DataFrame, df_all: pd.DataFrame):
|
| 684 |
+
best = {"filter_thin": None, "filter_mf": None, "all_thin": None, "all_mf": None}
|
| 685 |
+
if len(df_ok):
|
| 686 |
+
best["filter_thin"] = df_ok.loc[df_ok["THINNING"].idxmin()]
|
| 687 |
+
best["filter_mf"] = df_ok.loc[df_ok["MAX_FAILURE"].idxmin()]
|
| 688 |
+
best["all_thin"] = df_all.loc[df_all["THINNING"].idxmin()]
|
| 689 |
+
best["all_mf"] = df_all.loc[df_all["MAX_FAILURE"].idxmin()]
|
| 690 |
+
return best
|
| 691 |
+
|
| 692 |
+
best = _best_rows(matched if len(matched) else df_all, df_all)
|
| 693 |
+
st.session_state.best_filter_thin = None if matched.empty else best["filter_thin"].to_dict()
|
| 694 |
+
st.session_state.best_filter_mf = None if matched.empty else best["filter_mf"].to_dict()
|
| 695 |
+
st.session_state.best_all_thin = best["all_thin"].to_dict()
|
| 696 |
+
st.session_state.best_all_mf = best["all_mf"].to_dict()
|
| 697 |
+
|
| 698 |
+
if len(matched) and st.session_state.get("apply_post_filter", False):
|
| 699 |
+
st.session_state.sim_result = {
|
| 700 |
+
"THINNING": float(best["filter_thin"]["THINNING"]),
|
| 701 |
+
"MAX_FAILURE": float(best["filter_mf"]["MAX_FAILURE"]),
|
| 702 |
+
}
|
| 703 |
+
st.session_state.topcard_source = "filter"
|
| 704 |
+
else:
|
| 705 |
+
st.session_state.sim_result = {
|
| 706 |
+
"THINNING": float(best["all_thin"]["THINNING"]),
|
| 707 |
+
"MAX_FAILURE": float(best["all_mf"]["MAX_FAILURE"]),
|
| 708 |
+
}
|
| 709 |
+
st.session_state.topcard_source = "overall"
|
| 710 |
+
|
| 711 |
+
if len(matched):
|
| 712 |
+
now_str = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 713 |
+
matched = matched.copy()
|
| 714 |
+
matched["์ ํ"] = False
|
| 715 |
+
matched["Index"] = None
|
| 716 |
+
matched["์ ์ฅ์๊ฐ"] = now_str
|
| 717 |
+
matched["์ฌ์ง"] = matched["material"].astype(int).astype(str)
|
| 718 |
+
|
| 719 |
+
start_idx = len(st.session_state.history)
|
| 720 |
+
for i in range(len(matched)):
|
| 721 |
+
matched.at[matched.index[i],"Index"] = start_idx + i + 1
|
| 722 |
+
|
| 723 |
+
matched = matched[[
|
| 724 |
+
"์ ํ","Index","์ ์ฅ์๊ฐ","bead",
|
| 725 |
+
"thickness","์ฌ์ง","diameter","degree","upper_radius","lower_radius",
|
| 726 |
+
"THINNING","MAX_FAILURE"
|
| 727 |
+
]].rename(columns={
|
| 728 |
+
"bead":"๋น๋ ํ์
",
|
| 729 |
+
"thickness":"์์ฌ ๋๊ป (mm)",
|
| 730 |
+
"upper_radius":"์๋จ R",
|
| 731 |
+
"lower_radius":"ํ๋จ R",
|
| 732 |
+
})
|
| 733 |
+
st.session_state.history.extend(matched.to_dict("records"))
|
| 734 |
+
if st.session_state.get("apply_post_filter", False):
|
| 735 |
+
st.success(f"โ
์ด {total}๊ฐ ์กฐํฉ ์ค **ํํฐ๋ฅผ ๋ง์กฑํ {len(matched)}๊ฐ**๋ฅผ ๊ธฐ๋ก์ ์ถ๊ฐํ์ต๋๋ค.")
|
| 736 |
+
else:
|
| 737 |
+
st.success(f"โ
ํํฐ ๋ฏธ์ ์ฉ: **์ด {len(matched)}๊ฐ ์ ์ฒด ์กฐํฉ**์ ๊ธฐ๋ก์ ์ถ๊ฐํ์ต๋๋ค.")
|
| 738 |
+
else:
|
| 739 |
+
if st.session_state.get("apply_post_filter", False):
|
| 740 |
+
st.warning(f"ํํฐ ์กฐ๊ฑด์ ๋ง์กฑํ๋ ์กฐํฉ์ด ์์ต๋๋ค. (์ด {total}๊ฐ ์กฐํฉ)")
|
| 741 |
+
else:
|
| 742 |
+
st.warning("๋จ๋ ์กฐํฉ์ด ์์ต๋๋ค.")
|
| 743 |
+
|
| 744 |
+
# -----------------------------------------
|
| 745 |
# 2) ๊ฒฐ๊ณผ ์๊ฐํ
|
| 746 |
+
# -----------------------------------------
|
| 747 |
with tabs[1]:
|
| 748 |
st.header("๊ฒฐ๊ณผ ์๊ฐํ")
|
| 749 |
+
|
| 750 |
+
# (์ ํ) ์ด ํญ์์ hr/st.divider๋ฅผ ์จ๊ธฐ๊ณ ์ถ์ผ๋ฉด ์ฃผ์ ํด์
|
| 751 |
+
# st.markdown("""
|
| 752 |
+
# <style>#results-tab hr, #results-tab .stDivider{display:none!important}</style>
|
| 753 |
+
# <div id="results-tab">""", unsafe_allow_html=True)
|
| 754 |
+
|
| 755 |
result_data = st.session_state.get("sim_result", DEFAULT_RESULT)
|
| 756 |
+
src = st.session_state.get("topcard_source", "")
|
| 757 |
+
if src == "filter": st.caption("์นด๋ ๊ฐ: **ํํฐ ๋ด ์ต์ ** (THINNING ์ต์ / MAX FAILURE ์ต์)")
|
| 758 |
+
elif src == "overall": st.caption("์นด๋ ๊ฐ: **์ ์ฒด ํ์ ์ต์ ** (THINNING ์ต์ / MAX FAILURE ์ต์)")
|
| 759 |
+
elif src == "single": st.caption("์นด๋ ๊ฐ: **๋จ์ผ ์
๋ ฅ ๊ฒฐ๊ณผ**")
|
| 760 |
+
|
| 761 |
+
col1, col2 = st.columns(2, gap="small")
|
| 762 |
+
with col1:
|
| 763 |
+
st.session_state.setdefault("material", "590")
|
| 764 |
+
cur_mat = st.session_state["material"]
|
| 765 |
+
thin_cap = MATERIAL_THICKNESS_CAP.get(cur_mat, 0.16)
|
| 766 |
+
st.session_state["thinning_min"] = 0.0
|
| 767 |
+
st.session_state["thinning_max"] = thin_cap
|
| 768 |
+
metric_card("THINNING (๋๊ป ๊ฐ์์จ)",
|
| 769 |
+
result_data.get("THINNING", 0.0),
|
| 770 |
+
float(st.session_state["thinning_min"]),
|
| 771 |
+
float(st.session_state["thinning_max"]))
|
| 772 |
+
with col2:
|
| 773 |
+
st.session_state.setdefault("max_failure_min", 0.00)
|
| 774 |
+
st.session_state.setdefault("max_failure_max", 0.97)
|
| 775 |
+
metric_card("MAX FAILURE",
|
| 776 |
+
result_data.get("MAX_FAILURE", 0.0),
|
| 777 |
+
float(st.session_state["max_failure_min"]),
|
| 778 |
+
float(st.session_state["max_failure_max"]))
|
| 779 |
+
|
| 780 |
+
cL, cR = st.columns(2, gap="small")
|
| 781 |
+
|
| 782 |
+
def _summary_block(title: str, row_thin: pd.Series, row_mf: pd.Series):
|
| 783 |
+
st.subheader(title)
|
| 784 |
+
st.markdown("**THINNING ์ต์**")
|
| 785 |
+
if row_thin is not None and len(row_thin):
|
| 786 |
+
lb, rb = int(row_thin.get('LB',0)), int(row_thin.get('RB',0))
|
| 787 |
+
bead_label = ("No Bead" if (lb==0 and rb==0) else ("Left Bead" if (lb==1 and rb==0)
|
| 788 |
+
else ("Right Bead" if (lb==0 and rb==1) else "Double Bead")))
|
| 789 |
+
st.markdown(
|
| 790 |
+
f"- ์ฌ์ง: **{row_thin.get('material','-')}** (๋น๋: **{bead_label}**)<br>"
|
| 791 |
+
f"- ๋๊ป: **{row_thin.get('thickness','-')} mm**, ์ง๊ฒฝ: **{row_thin.get('diameter','-')} mm**, ๊ฐ๋: **{row_thin.get('degree','-')}ยฐ**<br>"
|
| 792 |
+
f"- ์๋จ R: **{row_thin.get('upper_radius','-')}**, ํ๋จ R: **{row_thin.get('lower_radius','-')}**<br>"
|
| 793 |
+
f"- **THINNING: {row_thin.get('THINNING',0):.3f}**, MAX_FAILURE: {row_thin.get('MAX_FAILURE',0):.3f}",
|
| 794 |
+
unsafe_allow_html=True)
|
| 795 |
+
else:
|
| 796 |
+
st.caption("ํด๋น ์์")
|
| 797 |
+
st.markdown("---")
|
| 798 |
+
st.markdown("**MAX_FAILURE ์ต์**")
|
| 799 |
+
if row_mf is not None and len(row_mf):
|
| 800 |
+
lb, rb = int(row_mf.get('LB',0)), int(row_mf.get('RB',0))
|
| 801 |
+
bead_label = ("No Bead" if (lb==0 and rb==0) else ("Left Bead" if (lb==1 and rb==0)
|
| 802 |
+
else ("Right Bead" if (lb==0 and rb==1) else "Double Bead")))
|
| 803 |
+
st.markdown(
|
| 804 |
+
f"- ์ฌ์ง: **{row_mf.get('material','-')}** (๋น๋: **{bead_label}**)<br>"
|
| 805 |
+
f"- ๋๊ป: **{row_mf.get('thickness','-')} mm**, ์ง๊ฒฝ: **{row_mf.get('diameter','-')} mm**, ๊ฐ๋: **{row_mf.get('degree','-')}ยฐ**<br>"
|
| 806 |
+
f"- ์๋จ R: **{row_mf.get('upper_radius','-')}**, ํ๋จ R: **{row_mf.get('lower_radius','-')}**<br>"
|
| 807 |
+
f"- THINNING: {row_mf.get('THINNING',0):.3f}, **MAX_FAILURE: {row_mf.get('MAX_FAILURE',0):.3f}**",
|
| 808 |
+
unsafe_allow_html=True)
|
| 809 |
+
else:
|
| 810 |
+
st.caption("ํด๋น ์์")
|
| 811 |
+
|
| 812 |
+
with cL:
|
| 813 |
+
_summary_block(
|
| 814 |
+
"ํํฐ ๋ด ์ต์ ",
|
| 815 |
+
None if st.session_state.get("best_filter_thin") is None else pd.Series(st.session_state["best_filter_thin"]),
|
| 816 |
+
None if st.session_state.get("best_filter_mf") is None else pd.Series(st.session_state["best_filter_mf"]),
|
| 817 |
+
)
|
| 818 |
+
with cR:
|
| 819 |
+
_summary_block(
|
| 820 |
+
"์ ์ฒด ํ์ ์ต์ ",
|
| 821 |
+
pd.Series(st.session_state.get("best_all_thin", {})) if st.session_state.get("best_all_thin") else None,
|
| 822 |
+
pd.Series(st.session_state.get("best_all_mf", {})) if st.session_state.get("best_all_mf") else None,
|
| 823 |
+
)
|
| 824 |
+
|
| 825 |
+
# โ
๊ธฐ์ค ์ฌ์ง & ์ํํ โ ๊ฐ์ ์์น(ํ ์ด)์ ์ธ๋ก๋ก ์ ๋ ฌ
|
| 826 |
+
with st.container():
|
| 827 |
+
material_list = DISPLAY_LABELS
|
| 828 |
+
cur = st.session_state.get("material", "590")
|
| 829 |
+
default_idx = material_list.index(cur) if cur in material_list else 1
|
| 830 |
+
|
| 831 |
+
sel = st.selectbox("๊ธฐ์ค ์ฌ์ง", material_list, index=default_idx, key="material_for_result")
|
| 832 |
+
st.session_state["material"] = sel
|
| 833 |
+
|
| 834 |
+
cap = MATERIAL_THICKNESS_CAP[sel]
|
| 835 |
+
st.session_state["thinning_min"] = 0.0
|
| 836 |
+
st.session_state["thinning_max"] = cap
|
| 837 |
+
st.caption(f"ํ์ฌ ๊ธฐ์ค ์ฌ์ง: {sel} (๋๊ป ๊ฐ์์จ ์ํ {cap:.2f})")
|
| 838 |
|
| 839 |
+
# ๋ฐ๋ก ์๋์ ์ํ ๊ธฐ์คํ๋ฅผ ๋ถ์ฌ์ ํ์
|
| 840 |
+
st.markdown('<div class="tight-block">', unsafe_allow_html=True)
|
| 841 |
+
st.caption("์ฌ์ง๋ณ ๋๊ป ๊ฐ์์จ ์ํ")
|
| 842 |
+
render_cap_table()
|
| 843 |
+
st.markdown('</div>', unsafe_allow_html=True)
|
| 844 |
|
| 845 |
+
|
| 846 |
+
# (์ ํ) ์์์ ์ด์์ผ๋ฉด ๋ซ๊ธฐ
|
| 847 |
+
# st.markdown("</div>", unsafe_allow_html=True)
|
| 848 |
+
|
| 849 |
+
|
| 850 |
+
|
| 851 |
+
# -----------------------------------------
|
| 852 |
+
# 3) ๊ธฐ๋ก ์กฐํ โ
์ ์ฒด ๊ต์ฒด
|
| 853 |
+
# -----------------------------------------
|
| 854 |
with tabs[2]:
|
| 855 |
st.header("๊ธฐ๋ก ์กฐํ")
|
| 856 |
|
| 857 |
+
col1, col2, col3 = st.columns([1, 1, 1])
|
| 858 |
with col1:
|
| 859 |
save_btn = st.button("ํ์ฌ ๊ฒฐ๊ณผ ์ ์ฅ", type="primary", use_container_width=True)
|
| 860 |
with col2:
|
| 861 |
select_all_btn = st.button("์ ์ฒด ์ ํ", use_container_width=True)
|
| 862 |
with col3:
|
| 863 |
delete_btn = st.button("์ ํ ํญ๋ชฉ ์ญ์ ", use_container_width=True)
|
|
|
|
|
|
|
| 864 |
|
| 865 |
+
# ===== ํ์ฌ ๊ฒฐ๊ณผ ์ ์ฅ ์ฒ๋ฆฌ =====
|
| 866 |
if save_btn:
|
| 867 |
if "sim_result" not in st.session_state:
|
| 868 |
st.warning("๋จผ์ ์๋ฎฌ๋ ์ด์
์ ์คํํ์ธ์.")
|
| 869 |
else:
|
| 870 |
+
# ๋ค์ ์ธ๋ฑ์ค
|
| 871 |
+
try:
|
| 872 |
+
next_idx = max([r.get("Index", 0) for r in st.session_state.history]) + 1 if st.session_state.history else 1
|
| 873 |
+
except Exception:
|
| 874 |
+
next_idx = len(st.session_state.history) + 1
|
| 875 |
+
|
| 876 |
+
# ์
๋ ฅ ๋ชจ๋/๋ผ๋ฒจ
|
| 877 |
+
input_mode = st.session_state.get("input_mode", "์ง์ ์
๋ ฅ")
|
| 878 |
+
if input_mode == "์ง์ ์
๋ ฅ":
|
| 879 |
+
bead_label = st.session_state.get("beadType", "-")
|
| 880 |
+
material_label = st.session_state.get("material", "-")
|
| 881 |
+
else:
|
| 882 |
+
bead_label = ", ".join(st.session_state.get("beadTypes_multi", [])) or "-"
|
| 883 |
+
material_label = ", ".join(st.session_state.get("materials_multi", [])) or "-"
|
| 884 |
+
|
| 885 |
+
# ๋จ์ผ ๊ฐ๋ ์์ผ๋ฉด ํจ๊ป ์ ์ฅ(ํ์์ ํํฐ/์ ๋ ฌ ํธํ๊ฒ)
|
| 886 |
+
diameter_val = st.session_state.get("diameter")
|
| 887 |
+
degree_val = st.session_state.get("degree")
|
| 888 |
+
|
| 889 |
+
new_row = {
|
| 890 |
"์ ํ": False,
|
| 891 |
+
"Index": next_idx,
|
| 892 |
"์ ์ฅ์๊ฐ": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
| 893 |
+
"๋น๋ ํ์
": bead_label,
|
| 894 |
+
"์
๋ ฅ ๋ฐฉ์": input_mode,
|
| 895 |
"์์ฌ ๋๊ป (mm)": val_or_range("thickness", "thicknessRange", " mm"),
|
| 896 |
+
"์ฌ์ง": material_label,
|
| 897 |
"์ง๊ฒฝ": val_or_range("diameter", "diameterRange", " mm"),
|
| 898 |
"๊ฐ๋": val_or_range("degree", "degreeRange", "ยฐ"),
|
| 899 |
"์๋จ R": val_or_range("upperR", "upperRRange"),
|
| 900 |
"ํ๋จ R": val_or_range("lowerR", "lowerRRange"),
|
| 901 |
+
"THINNING": float(st.session_state.sim_result.get("THINNING")),
|
| 902 |
+
"MAX_FAILURE": float(st.session_state.sim_result.get("MAX_FAILURE")),
|
| 903 |
+
# ์ฐธ๊ณ ์ฉ ์์ ์ซ์(์์ผ๋ฉด None)
|
| 904 |
+
"diameter": diameter_val,
|
| 905 |
+
"degree": degree_val,
|
| 906 |
}
|
|
|
|
|
|
|
| 907 |
|
| 908 |
+
st.session_state.history.append(new_row)
|
| 909 |
+
# ์ฆ์ ๋ฐ์
|
| 910 |
+
st.success("ํ์ฌ ๊ฒฐ๊ณผ๊ฐ ๊ธฐ๋ก์ ์ ์ฅ๋์์ต๋๋ค.")
|
| 911 |
+
st.rerun()
|
|
|
|
|
|
|
|
|
|
| 912 |
|
| 913 |
+
# ===== ํ
์ด๋ธ/๋ฒํผ ๋์ =====
|
| 914 |
if st.session_state.history:
|
| 915 |
df = pd.DataFrame(st.session_state.history)
|
| 916 |
cols = ["์ ํ"] + [c for c in df.columns if c != "์ ํ"]
|
| 917 |
df = df[cols]
|
| 918 |
|
| 919 |
if select_all_btn:
|
| 920 |
+
for r in st.session_state.history:
|
| 921 |
+
r["์ ํ"] = True
|
| 922 |
+
st.rerun()
|
| 923 |
|
| 924 |
st.subheader(f"๊ธฐ๋ก ํ
์ด๋ธ (์ด {len(df)}๊ฑด, ์ฒดํฌ ํ ์ญ์ ๊ฐ๋ฅ)")
|
| 925 |
+
edited_df = st.data_editor(
|
| 926 |
+
df, hide_index=True, use_container_width=True, key="history_editor"
|
| 927 |
+
)
|
| 928 |
|
| 929 |
if delete_btn:
|
| 930 |
selected_index = edited_df[edited_df["์ ํ"] == True]["Index"].tolist()
|
| 931 |
st.session_state.history = [
|
| 932 |
+
rec for rec in st.session_state.history if rec.get("Index") not in selected_index
|
| 933 |
]
|
| 934 |
st.success(f"{len(selected_index)}๊ฐ ํญ๋ชฉ ์ญ์ ์๋ฃ!")
|
| 935 |
st.rerun()
|
| 936 |
+
|
| 937 |
+
# CSV ๋ค์ด๋ก๋(์ ์ฒด/์ ํ)
|
| 938 |
+
sel_df = edited_df[edited_df["์ ํ"] == True].copy()
|
| 939 |
+
c1, c2 = st.columns(2)
|
| 940 |
+
with c1:
|
| 941 |
+
csv_all = edited_df.to_csv(index=False).encode("utf-8-sig")
|
| 942 |
+
st.download_button("CSV (์ ์ฒด ๋ค์ด๋ก๋)", csv_all, "simulation_history_all.csv", "text/csv", use_container_width=True)
|
| 943 |
+
with c2:
|
| 944 |
+
if len(sel_df):
|
| 945 |
+
csv_sel = sel_df.to_csv(index=False).encode("utf-8-sig")
|
| 946 |
+
st.download_button("CSV (์ ํ๋ง ๋ค์ด๋ก๋)", csv_sel, "simulation_history_selected.csv", "text/csv", use_container_width=True)
|
| 947 |
+
else:
|
| 948 |
+
st.caption("์ ํ๋ ํ์ด ์์ต๋๋ค. ํ์์ ์ฒดํฌ ํ ๋ค์ด๋ก๋ํ์ธ์.")
|
| 949 |
else:
|
| 950 |
+
st.info("์์ง ์ ์ฅ๋ ๊ธฐ๋ก์ด ์์ต๋๋ค. ๋ฒ์ ์
๋ ฅ์ผ๋ก ์คํํ๋ฉด ์กฐ๊ฑด์ ๋ง์กฑํ ๋ชจ๋ ์กฐํฉ์ด ์๋ ์ ์ฅ๋ฉ๋๋ค.")
|