Geo-Lab / app /main.py
HANSOL
v4.3: Multi-Page structure + 5 new landforms (uvala, tower_karst, karren, transverse_dune, star_dune)
2be4f60
"""
Geo-Lab AI v4.0: ๋‹ค์ค‘ ์ด๋ก  ๋ชจ๋ธ + ์‚ฌ์‹ค์  ๋ Œ๋”๋ง
๊ฐ ์ง€ํ˜•์— ๋Œ€ํ•ด ์—ฌ๋Ÿฌ ์ด๋ก ์„ ์„ ํƒํ•˜๊ณ  ๋น„๊ตํ•  ์ˆ˜ ์žˆ์Œ
"""
import streamlit as st
import numpy as np
import matplotlib.pyplot as plt
from matplotlib import cm, colors
from matplotlib.colors import LightSource
import matplotlib.patches as mpatches
import sys
import os
import time
from PIL import Image
# ์—”์ง„ ์ž„ํฌํŠธ
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
try:
from engine.pyvista_render import (
PYVISTA_AVAILABLE, render_v_valley_pyvista,
render_delta_pyvista, render_meander_pyvista
)
import pyvista as pv
from stpyvista import stpyvista
STPYVISTA_AVAILABLE = True
except ImportError:
PYVISTA_AVAILABLE = False
STPYVISTA_AVAILABLE = False
# Plotly (์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D)
import plotly.graph_objects as go
# ํ†ตํ•ฉ ๋ฌผ๋ฆฌ ์—”์ง„ ์ž„ํฌํŠธ (Phase 5)
from engine.grid import WorldGrid
from engine.fluids import HydroKernel
from engine.fluids import HydroKernel
from engine.erosion_process import ErosionProcess
from engine.script_engine import ScriptExecutor
from engine.system import EarthSystem
from engine.ideal_landforms import IDEAL_LANDFORM_GENERATORS, ANIMATED_LANDFORM_GENERATORS, create_delta, create_alluvial_fan, create_meander, create_u_valley, create_v_valley, create_barchan_dune, create_coastal_cliff
# ํŽ˜์ด์ง€ ์„ค์ •
st.set_page_config(
page_title="๐ŸŒŠ Geo-Lab AI v4",
page_icon="๐ŸŒŠ",
layout="wide"
)
# CSS
st.markdown("""
<style>
.main-header { font-size: 2rem; font-weight: bold; color: #1565C0; }
.theory-card {
background: linear-gradient(135deg, #E3F2FD, #BBDEFB);
padding: 1rem; border-radius: 10px; margin: 0.5rem 0;
border-left: 4px solid #1565C0;
}
.formula {
font-family: 'Courier New', monospace;
background: #263238; color: #80CBC4;
padding: 0.3rem 0.6rem; border-radius: 4px;
display: inline-block;
}
.theory-title { font-weight: bold; color: #1565C0; font-size: 1.1rem; }
</style>
""", unsafe_allow_html=True)
# ============ ์ด๋ก  ์ •์˜ ============
V_VALLEY_THEORIES = {
"Stream Power Law": {
"formula": "E = K ร— A^m ร— S^n",
"description": "์œ ๋Ÿ‰(A)๊ณผ ๊ฒฝ์‚ฌ(S)์˜ ๊ฑฐ๋“ญ์ œ๊ณฑ ๊ด€๊ณ„๋กœ ์นจ์‹๋ฅ  ๊ณ„์‚ฐ. ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ๋˜๋Š” ๋ชจ๋ธ.",
"params": ["K (์นจ์‹๊ณ„์ˆ˜)", "m (๋ฉด์ ์ง€์ˆ˜, 0.3-0.6)", "n (๊ฒฝ์‚ฌ์ง€์ˆ˜, 1.0-2.0)"],
"key": "stream_power"
},
"Shear Stress Model": {
"formula": "E = K ร— (ฯ„ - ฯ„c)^a",
"description": "ํ•˜์ฒœ ๋ฐ”๋‹ฅ์˜ ์ „๋‹จ์‘๋ ฅ(ฯ„)์ด ์ž„๊ณ„๊ฐ’(ฯ„c)์„ ์ดˆ๊ณผํ•  ๋•Œ ์นจ์‹ ๋ฐœ์ƒ.",
"params": ["K (์นจ์‹๊ณ„์ˆ˜)", "ฯ„c (์ž„๊ณ„ ์ „๋‹จ์‘๋ ฅ)", "a (์ง€์ˆ˜)"],
"key": "shear_stress"
},
"Detachment-Limited": {
"formula": "E = K ร— A^m ร— S^n ร— (1 - Qs/Qc)",
"description": "ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰๋Ÿ‰(Qs)์ด ์šด๋ฐ˜๋Šฅ๋ ฅ(Qc)๋ณด๋‹ค ์ž‘์„ ๋•Œ๋งŒ ์นจ์‹. ์•”์„ ๋ถ„๋ฆฌ ์†๋„ ์ œํ•œ.",
"params": ["K (๋ถ„๋ฆฌ๊ณ„์ˆ˜)", "Qc (์šด๋ฐ˜๋Šฅ๋ ฅ)"],
"key": "detachment"
}
}
MEANDER_THEORIES = {
"Helical Flow (Rozovskii)": {
"formula": "V_r = (Vยฒ/gR) ร— h",
"description": "๊ณก๋ฅ˜์—์„œ ์›์‹ฌ๋ ฅ์— ์˜ํ•ด ๋‚˜์„ ํ˜• 2์ฐจ๋ฅ˜ ๋ฐœ์ƒ. ๋ฐ”๊นฅ์ชฝ ํ‘œ๋ฉด๋ฅ˜, ์•ˆ์ชฝ ๋ฐ”๋‹ฅ๋ฅ˜.",
"params": ["V (์œ ์†)", "R (๊ณก๋ฅ ๋ฐ˜๊ฒฝ)", "h (์ˆ˜์‹ฌ)"],
"key": "helical"
},
"Ikeda-Parker-Sawai Model": {
"formula": "โˆ‚ฮท/โˆ‚t = Eโ‚€ ร— U ร— (H/Hโ‚€)^ฮฑ ร— ฯ‡",
"description": "ํ•˜์•ˆ ์นจ์‹๋ฅ ์ด ์œ ์†(U), ์ˆ˜์‹ฌ(H), ๊ณก๋ฅ (ฯ‡)์˜ ํ•จ์ˆ˜. ๊ณก๋ฅ˜ ์ง„ํ™”์˜ ํ‘œ์ค€ ๋ชจ๋ธ.",
"params": ["Eโ‚€ (์นจ์‹๊ณ„์ˆ˜)", "Hโ‚€ (๊ธฐ์ค€์ˆ˜์‹ฌ)", "ฮฑ (์ง€์ˆ˜)"],
"key": "ikeda_parker"
},
"Seminara Bar Model": {
"formula": "ฮป = ฮฒ ร— W ร— Fr^ฮณ",
"description": "ํฌ์ธํŠธ๋ฐ” ํ˜•์„ฑ๊ณผ ์ฑ„๋„ ์ด๋™์˜ ๊ฒฐํ•ฉ ๋ชจ๋ธ. ๋ฐ”์˜ ํŒŒ์žฅ(ฮป)์ด ์ฑ„๋„ํญ(W)๊ณผ Froude์ˆ˜(Fr)์— ๋น„๋ก€.",
"params": ["ฮฒ (๋น„๋ก€์ƒ์ˆ˜)", "ฮณ (์ง€์ˆ˜)", "Fr (Froude์ˆ˜)"],
"key": "seminara"
}
}
DELTA_THEORIES = {
"Galloway Classification": {
"formula": "ฮ” = f(River, Wave, Tidal)",
"description": "ํ•˜์ฒœยทํŒŒ๋ž‘ยท์กฐ๋ฅ˜ 3๊ฐ€์ง€ ์—๋„ˆ์ง€ ๊ท ํ˜•์œผ๋กœ ์‚ผ๊ฐ์ฃผ ํ˜•ํƒœ ๊ฒฐ์ •. ๊ฐ€์žฅ ๋„๋ฆฌ ์‚ฌ์šฉ.",
"params": ["ํ•˜์ฒœ ์—๋„ˆ์ง€", "ํŒŒ๋ž‘ ์—๋„ˆ์ง€", "์กฐ๋ฅ˜ ์—๋„ˆ์ง€"],
"key": "galloway"
},
"Orton-Reading Model": {
"formula": "ฮ” = f(Grain, Wave, Tidal)",
"description": "ํ‡ด์ ๋ฌผ ์ž…์ž ํฌ๊ธฐ์™€ ํ•ด์–‘ ์—๋„ˆ์ง€๋ฅผ ๊ณ ๋ ค. ์„ธ๋ฆฝ์งˆ/์กฐ๋ฆฝ์งˆ ์‚ผ๊ฐ์ฃผ ๊ตฌ๋ถ„.",
"params": ["์ž…์žํฌ๊ธฐ", "ํŒŒ๋ž‘ ์—๋„ˆ์ง€", "์กฐ๋ฅ˜ ์—๋„ˆ์ง€"],
"key": "orton"
},
"Bhattacharya Model": {
"formula": "ฮ” = f(Qsed, Hs, Tr)",
"description": "ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰๋Ÿ‰(Qsed), ์œ ์˜ํŒŒ๊ณ (Hs), ์กฐ์ฐจ(Tr)์˜ ์ •๋Ÿ‰์  ๋ชจ๋ธ.",
"params": ["Qsed (ํ‡ด์ ๋ฌผ๋Ÿ‰)", "Hs (ํŒŒ๊ณ )", "Tr (์กฐ์ฐจ)"],
"key": "bhattacharya"
}
}
# ===== ํ•ด์•ˆ ์ง€ํ˜• ์ด๋ก  =====
COASTAL_THEORIES = {
"Wave Erosion (Sunamura)": {
"formula": "E = K ร— H^a ร— T^b",
"description": "ํŒŒ๊ณ (H)์™€ ์ฃผ๊ธฐ(T)์— ๋”ฐ๋ฅธ ํ•ด์‹์•  ์นจ์‹๋ฅ . ํ•ด์‹์•  ํ›„ํ‡ด์˜ ๊ธฐ๋ณธ ๋ชจ๋ธ.",
"params": ["H (ํŒŒ๊ณ )", "T (ํŒŒ ์ฃผ๊ธฐ)", "K (์•”์„ ์ €ํ•ญ๊ณ„์ˆ˜)"],
"key": "wave_erosion"
},
"Cliff Retreat Model": {
"formula": "R = Eโ‚€ ร— (H/Hc)^n",
"description": "์ž„๊ณ„ํŒŒ๊ณ (Hc) ์ดˆ๊ณผ ์‹œ ํ•ด์‹์•  ํ›„ํ‡ด. ๋…ธ์น˜ ํ˜•์„ฑ๊ณผ ๋ถ•๊ดด ์‚ฌ์ดํด.",
"params": ["Eโ‚€ (๊ธฐ์ค€ ํ›„ํ‡ด์œจ)", "Hc (์ž„๊ณ„ํŒŒ๊ณ )", "n (์ง€์ˆ˜)"],
"key": "cliff_retreat"
},
"CERC Transport": {
"formula": "Q = K ร— Hยฒ{b} ร— sin(2ฮธ)",
"description": "์—ฐ์•ˆ๋ฅ˜์— ์˜ํ•œ ๋ชจ๋ž˜ ์ด๋™. ์‚ฌ๋นˆ, ์‚ฌ์ทจ, ์‚ฌ์ฃผ ํ˜•์„ฑ์˜ ๊ธฐ๋ณธ ๋ชจ๋ธ.",
"params": ["H_b (์‡„ํŒŒ ํŒŒ๊ณ )", "ฮธ (ํŒŒํ–ฅ๊ฐ)", "K (์ˆ˜์†ก๊ณ„์ˆ˜)"],
"key": "cerc"
},
"Spit & Lagoon": {
"formula": "Qs = H^2.5 * sin(2ฮฑ)",
"description": "์—ฐ์•ˆ๋ฅ˜์— ์˜ํ•ด ๋ชจ๋ž˜๊ฐ€ ๊ณถ ๋์—์„œ ๋ป—์–ด๋‚˜๊ฐ€ ์‚ฌ์ทจ์™€ ์„ํ˜ธ ํ˜•์„ฑ.",
"params": ["์—ฐ์•ˆ๋ฅ˜ ๊ฐ•๋„", "๋ชจ๋ž˜ ๊ณต๊ธ‰", "ํŒŒํ–ฅ"],
"key": "spit"
},
"Tombolo": {
"formula": "Kd = H_diff / H_inc",
"description": "์„ฌ ํ›„๋ฉด์˜ ํŒŒ๋ž‘ ํšŒ์ ˆ๋กœ ์ธํ•œ ๋ชจ๋ž˜ ํ‡ด์ . ์œก๊ณ„๋„ ํ˜•์„ฑ.",
"params": ["์„ฌ ๊ฑฐ๋ฆฌ", "ํŒŒ๋ž‘ ์—๋„ˆ์ง€", "์„ฌ ํฌ๊ธฐ"],
"key": "tombolo"
},
"Tidal Flat": {
"formula": "D = C * ws * (1 - ฯ„/ฯ„d)",
"description": "์กฐ์ˆ˜ ๊ฐ„๋งŒ์˜ ์ฐจ๋กœ ํ˜•์„ฑ๋˜๋Š” ๊ด‘ํ™œํ•œ ๊ฐฏ๋ฒŒ.",
"params": ["์กฐ์ฐจ(Tidal Range)", "ํ‡ด์ ๋ฌผ ๋†๋„"],
"key": "tidal_flat"
}
}
# ===== ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜• ์ด๋ก  =====
KARST_THEORIES = {
"Chemical Weathering": {
"formula": "CaCOโ‚ƒ + Hโ‚‚O + COโ‚‚ โ†’ Ca(HCOโ‚ƒ)โ‚‚",
"description": "ํƒ„์‚ฐ์นผ์Š˜์˜ ํ™”ํ•™์  ์šฉ์‹. COโ‚‚ ๋†๋„์™€ ์ˆ˜์˜จ์— ๋”ฐ๋ผ ์šฉ์‹๋ฅ  ๋ณ€ํ™”.",
"params": ["COโ‚‚ ๋†๋„", "์ˆ˜์˜จ", "๊ฐ•์ˆ˜๋Ÿ‰"],
"key": "chemical"
},
"Doline Evolution": {
"formula": "V = Vโ‚€ ร— exp(kt)",
"description": "๋Œ๋ฆฌ๋„ค์˜ ์ง€์ˆ˜์  ์„ฑ์žฅ. ์‹œ๊ฐ„์— ๋”ฐ๋ผ ์šฐ๋ฐœ๋ผ, ํด๋ฆฌ์—๋กœ ๋ฐœ์ „.",
"params": ["์ดˆ๊ธฐ ํฌ๊ธฐ", "์„ฑ์žฅ๋ฅ ", "๋ณ‘ํ•ฉ ํ™•๋ฅ "],
"key": "doline"
},
"Cave Development": {
"formula": "D = f(Q, S, t)",
"description": "์ง€ํ•˜์ˆ˜ ์œ ๋Ÿ‰(Q)๊ณผ ๊ฒฝ์‚ฌ(S)์— ๋”ฐ๋ฅธ ๋™๊ตด ๋ฐœ๋‹ฌ. ์ข…์œ ์„/์„์ˆœ ํ˜•์„ฑ.",
"params": ["์ง€ํ•˜์ˆ˜ ์œ ๋Ÿ‰", "๊ฒฝ์‚ฌ", "์„ํšŒ์•” ๋‘๊ป˜"],
"key": "cave"
}
}
# ===== ํ™”์‚ฐ ์ง€ํ˜• ์ด๋ก  =====
VOLCANIC_THEORIES = {
"Effusive (Shield)": {
"formula": "H/R = f(ฮท)",
"description": "์ €์ ์„ฑ ํ˜„๋ฌด์•”์งˆ ์šฉ์•”. ์ˆœ์ƒํ™”์‚ฐ(๋ฐฉํŒจ ๋ชจ์–‘) ํ˜•์„ฑ. ํ•˜์™€์ด, ์ œ์ฃผ๋„.",
"params": ["์šฉ์•” ์ ์„ฑ", "๋ถ„์ถœ๋ฅ ", "๊ฒฝ์‚ฌ๊ฐ"],
"key": "shield"
},
"Explosive (Strato)": {
"formula": "VEI = logโ‚โ‚€(V)",
"description": "๊ณ ์ ์„ฑ ์•ˆ์‚ฐ์•”/์œ ๋ฌธ์•”. ์„ฑ์ธตํ™”์‚ฐ(์›์ถ”ํ˜•) ํ˜•์„ฑ. ํ›„์ง€์‚ฐ, ๋ฐฑ๋‘์‚ฐ.",
"params": ["ํญ๋ฐœ์ง€์ˆ˜(VEI)", "ํ™”์‚ฐ์žฌ๋Ÿ‰", "์šฉ์•”/ํ™”์‡„๋ฅ˜ ๋น„์œจ"],
"key": "strato"
},
"Caldera Formation": {
"formula": "D = f(Vmagma)",
"description": "๋งˆ๊ทธ๋งˆ ๋ฐฉ ๋น„์›€ ํ›„ ํ•จ๋ชฐ. ์นผ๋ฐ๋ผ ํ˜ธ์ˆ˜ ํ˜•์„ฑ. ๋ฐฑ๋‘์‚ฐ ์ฒœ์ง€.",
"params": ["๋งˆ๊ทธ๋งˆ ๋ฐฉ ํฌ๊ธฐ", "ํ•จ๋ชฐ ๊นŠ์ด"],
"key": "caldera"
}
}
# ===== ๋น™ํ•˜ ์ง€ํ˜• ์ด๋ก  =====
GLACIAL_THEORIES = {
"Glacial Erosion": {
"formula": "E = K ร— U ร— H",
"description": "๋น™ํ•˜ ์ด๋™์†๋„(U)์™€ ๋‘๊ป˜(H)์— ๋”ฐ๋ฅธ ์นจ์‹. V์ž๊ณกโ†’U์ž๊ณก ๋ณ€ํ˜•.",
"params": ["๋น™ํ•˜ ์†๋„", "๋น™ํ•˜ ๋‘๊ป˜", "์•”์„ ๊ฒฝ๋„"],
"key": "erosion"
},
"Fjord Development": {
"formula": "D = E ร— t + SLR",
"description": "๋น™ํ•˜ ์นจ์‹ ํ›„ ํ•ด์ˆ˜๋ฉด ์ƒ์Šน์œผ๋กœ ํ”ผ์˜ค๋ฅด ํ˜•์„ฑ. ๋…ธ๋ฅด์›จ์ด ํ•ด์•ˆ.",
"params": ["์นจ์‹ ๊นŠ์ด", "ํ•ด์ˆ˜๋ฉด ์ƒ์Šน"],
"key": "fjord"
},
"Moraine Deposition": {
"formula": "V = f(Qsed, Tmelting)",
"description": "๋น™ํ‡ด์„ ํ‡ด์ . ๋ถ„๊ธ‰ ๋ถˆ๋Ÿ‰ ํ‡ด์ ๋ฌผ. ๋“œ๋Ÿผ๋ฆฐ, ์—์Šค์ปค ํ˜•์„ฑ.",
"params": ["ํ‡ด์ ๋ฌผ๋Ÿ‰", "์œต๋น™ ์†๋„"],
"key": "moraine"
}
}
# ===== ๊ฑด์กฐ ์ง€ํ˜• ์ด๋ก  =====
ARID_THEORIES = {
"Barchan Dune": {
"formula": "H = 0.1 ร— L",
"description": "์ดˆ์Šน๋‹ฌ ๋ชจ์–‘ ์‚ฌ๊ตฌ. ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ๋ฟ”์ด ํ–ฅํ•จ. ๋‹จ์ผ ๋ฐ”๋žŒ ๋ฐฉํ–ฅ.",
"params": ["ํ’์†", "๋ชจ๋ž˜ ๊ณต๊ธ‰๋Ÿ‰", "๋ฐ”๋žŒ ๋ฐฉํ–ฅ"],
"key": "barchan"
},
"Mesa-Butte Evolution": {
"formula": "R = K ร— S ร— t",
"description": "๊ณ ์›(๋ฉ”์‚ฌ) โ†’ ํƒ์ƒ์ง€(๋ทฐํŠธ) โ†’ ์ฒจํƒ‘(์ŠคํŒŒ์ด์–ด) ์นจ์‹ ๋‹จ๊ณ„.",
"params": ["ํ›„ํ‡ด์œจ", "๊ฒฝ๋„ ์ฐจ์ด"],
"key": "mesa"
},
"Pediment Formation": {
"formula": "S = f(P, R)",
"description": "์‚ฐ์ง€ ๊ธฐ์Šญ์˜ ์™„๋งŒํ•œ ์•”๋ฐ˜ ํ‰ํƒ„๋ฉด. ํŽ˜๋””๋จผํŠธ + ๋ฐ”ํ•˜๋‹ค.",
"params": ["๊ฐ•์ˆ˜๋Ÿ‰", "์•”์„ ์ €ํ•ญ"],
"key": "pediment"
}
}
# ===== ํ‰์•ผ ์ง€ํ˜• ์ด๋ก  =====
PLAIN_THEORIES = {
"Floodplain Development": {
"formula": "A = f(Q, S, t)",
"description": "๋ฒ”๋žŒ์› ๋ฐœ๋‹ฌ. ์ž์—ฐ์ œ๋ฐฉ + ๋ฐฐํ›„์Šต์ง€ ํ˜•์„ฑ. ํ† ์ง€ ์ด์šฉ ๋ถ„ํ™”.",
"params": ["์œ ๋Ÿ‰", "๊ฒฝ์‚ฌ", "ํ‡ด์ ๋ฌผ๋Ÿ‰"],
"key": "floodplain"
},
"Levee-Backswamp": {
"formula": "H_levee > H_backswamp",
"description": "์ž์—ฐ์ œ๋ฐฉ(์กฐ๋ฆฝ์งˆ) vs ๋ฐฐํ›„์Šต์ง€(์„ธ๋ฆฝ์งˆ) ๋ถ„๊ธ‰. ๋…ผ/๋ฐญ ์ด์šฉ.",
"params": ["ํ‡ด์ ๋ฌผ ๋ถ„๊ธ‰", "๋ฒ”๋žŒ ๋นˆ๋„"],
"key": "levee"
},
"Alluvial Plain": {
"formula": "D = Qsed ร— t / A",
"description": "์ถฉ์ ํ‰์•ผ ํ˜•์„ฑ. ์„ ์ƒ์ง€ โ†’ ๋ฒ”๋žŒ์› โ†’ ์‚ผ๊ฐ์ฃผ ์—ฐ์†์ฒด.",
"params": ["ํ‡ด์ ๋ฌผ๋Ÿ‰", "์œ ์—ญ๋ฉด์ "],
"key": "alluvial"
}
}
# ============ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ํ•จ์ˆ˜๋“ค ============
@st.cache_data(ttl=3600)
def simulate_v_valley(theory: str, time_years: int, params: dict, grid_size: int = 80):
"""V์ž๊ณก ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (Hybrid Approach) - ๊ต๊ณผ์„œ์ ์ธ V์ž ๋‹จ๋ฉด ๊ฐ•์ œ"""
# [Hybrid Approach]
# ๋ฌผ๋ฆฌ ์—”์ง„์˜ ๋ถˆํ™•์‹ค์„ฑ์„ ์ œ๊ฑฐํ•˜๊ณ , ์™„๋ฒฝํ•œ V์ž๋ฅผ ๋ณด์—ฌ์ฃผ๊ธฐ ์œ„ํ•ด ํ˜•ํƒœ๋ฅผ ๊ฐ•์ œํ•จ.
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
center = cols // 2
# 1. Base Logic: Time-dependent Incision
# ์‹œ๊ฐ„์ด ์ง€๋‚ ์ˆ˜๋ก ๊นŠ์–ด์ง€๊ณ , V์ž๊ฐ€ ์„ ๋ช…ํ•ด์ง.
# U-Valley is U-shaped, V-Valley is V-shaped.
max_depth_possible = 150.0
# [Fix] Remove offset and scale faster for visualization
# 50,000 years to reach 50% depth, sat at 200k
current_depth = max_depth_possible * (1.0 - np.exp(-time_years / 50000.0))
# Rock Hardness affects width/steepness
rock_h = params.get('rock_hardness', 0.5)
# Hard rock -> Steep slope (Narrow V)
# Soft rock -> Gentle slope (Wide V)
valley_width_factor = 0.5 + (1.0 - rock_h) * 1.5 # 0.5(Hard) ~ 2.0(Soft)
# 2. Build Terrain
for r in range(rows):
# Longitudinal Profile (Downstream slope)
base_elev = 250.0 - (r / rows) * 60.0 # 250 -> 190
grid.bedrock[r, :] = base_elev
grid.update_elevation()
# 3. Carve V-Shape (Analytical)
x_coords = np.linspace(-500, 500, cols)
for c in range(cols):
dist_x = abs(c - center) # Distance from river center
dist_meters = dist_x * cell_size
# --- ํƒญ ๊ตฌ์„ฑ ---
tabs = st.tabs(["๐Ÿ”๏ธ ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ด์…˜", "๐Ÿ“œ ์Šคํฌ๋ฆฝํŠธ ๋žฉ", "๐ŸŒ Project Genesis (Unified Engine)"])
# [Tab 1] ๊ธฐ์กด ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (Legacy & Refactored)
with tabs[0]:
st.title("๐Ÿ”๏ธ ์ง€ํ˜• ํ˜•์„ฑ ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ (Geo-Landform Simulator)")
# ... (Existing content remains here) ...
# Need to indent existing content or just use 'with tabs[0]:' logic
# For this tool, I will just INSERT the new tab code at the END of the file or appropriate place.
# But wait, existing code structure is 'with st.sidebar... if mode == ...'
# The structure is messy.
# I should insert the NEW tab logic where tabs are defined.
# Let's verify where tabs are defined.
# Line 206: tabs = st.tabs(["์‹œ๋ฎฌ๋ ˆ์ด์…˜", "๊ฐค๋Ÿฌ๋ฆฌ", "์„ค์ •"]) -> Wait, viewed file didn't show this.
# Let's inspect main.py structure again quickly before editing.
pass
# [New Tab Logic Placeholder - Will replace in next step after verifying structure]function: Depth decreases linearly with distance
# z = z_base - max_depth * (1 - dist / width)
width_m = 400.0 * valley_width_factor
if dist_meters < width_m:
# Linear V-shape
incision_ratio = (1.0 - dist_meters / width_m)
# Make it slightly concave (power 1.2) for realism? Or strict V (power 1)?
# Textbook is strict V
incision = current_depth * incision_ratio
grid.bedrock[:, c] -= incision
# 4. Add Physics Noise (Textures)
# ํ•˜์ฒœ ๋ฐ”๋‹ฅ์— ์•ฝ๊ฐ„์˜ ๋ถˆ๊ทœ์น™์„ฑ
noise = np.random.rand(rows, cols) * 5.0
grid.bedrock += noise
# Wiggle the river center slightly? (Sinusuosity)
# V-valleys are usually straight-ish, but let's keep it simple.
grid.update_elevation()
# Calculate stats
depth = current_depth
x = np.linspace(0, 1000, cols)
# [Fix] Water Depth
water_depth = np.zeros_like(grid.elevation)
# V-valley bottom
river_w = 8
water_depth[:, center-river_w:center+river_w+1] = 2.0
return {'elevation': grid.elevation, 'depth': depth, 'x': x, 'water_depth': water_depth}
@st.cache_data(ttl=3600)
def simulate_meander(theory: str, time_years: int, params: dict, grid_size: int = 100):
"""
์ž์œ  ๊ณก๋ฅ˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (Process-Based)
- Kinoshita Curve๋กœ ๊ฒฝ๋กœ ์ƒ์„ฑ -> 3D ์ง€ํ˜•์— ์กฐ๊ฐ(Carving) & ํ‡ด์ (Deposition)
"""
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
# 1. ์ดˆ๊ธฐ ํ‰์•ผ (Floodplain)
# ์™„๋งŒํ•œ ๊ฒฝ์‚ฌ (์„œ -> ๋™ ํ๋ฆ„ ๊ฐ€์ • ํ˜น์€ ๋‚จ๋ถ?)
# ๊ธฐ์กด ์ฝ”๋“œ: x์ถ• ๋ฐฉํ–ฅ์œผ๋กœ ํ๋ฆ„
rows, cols = grid_size, grid_size
# ๊ธฐ๋ณธ ๊ณ ๋„: 50m
grid.bedrock[:] = 50.0
# Add slight slope W->E
X, Y = np.meshgrid(np.linspace(0, 1000, cols), np.linspace(0, 1000, rows))
grid.bedrock -= (X / 1000.0) * 5.0 # 5m drop over 1km
# 2. Kinoshita Curve Path Generation (Legacy Logic preserved for path)
n_points = 1000
s_vals = np.linspace(0, 20, n_points)
cycle_period = 100000
cycle_progress = (time_years % cycle_period) / cycle_period
# Amp grows then cutoff
max_theta = 2.2
theta_0 = 0.5 + cycle_progress * (max_theta - 0.5)
flattening = params.get('flattening', 0.2)
k_wavenumber = 1.0
# Current Path
theta = theta_0 * np.sin(k_wavenumber * s_vals) + (theta_0 * flattening) * np.sin(3 * k_wavenumber * s_vals)
dx = np.cos(theta)
dy = np.sin(theta)
x_path = np.cumsum(dx)
y_path = np.cumsum(dy)
# Rotate to flow Left->Right (W->E)
angle = np.arctan2(y_path[-1] - y_path[0], x_path[-1] - x_path[0])
target_angle = 0 # X-axis
rot_angle = target_angle - angle
rot_mat = np.array([[np.cos(rot_angle), -np.sin(rot_angle)],[np.sin(rot_angle), np.cos(rot_angle)]])
coords = np.vstack([x_path, y_path])
rotated = rot_mat @ coords
px = rotated[0, :]
py = rotated[1, :]
# Normalize to fit Grid (0-1000 with margins)
margin = 100
p_width = px.max() - px.min()
if p_width > 0:
scale = (1000 - 2*margin) / p_width
px = (px - px.min()) * scale + margin
py = py * scale
py = py - py.mean() + 500 # Center Y
# 3. Process-Based Terrain Modification
# A. Carve Channel (Subtractive)
# B. Deposit Point Bar (Additive - Inside Bend)
# C. Natural Levee (Additive - Banks)
channel_width = 30.0 # m
channel_depth = 5.0 # m
levee_height = 1.0 # m
levee_width = 20.0 # m
# Interpolate path for grid
# Map grid x,y to distance from channel
# Create distance field simplistic: for each grid point, find dist to curve? Too slow (100x100 * 1000).
# Faster: Draw curve onto grid mask.
grid.sediment[:] = 5.0 # Soil layer
# Iterate path points and carve
# Use finer resolution for drawing
for i in range(n_points):
cx, cy = px[i], py[i]
# Grid indices
c_idx = int(cx / cell_size)
r_idx = int(cy / cell_size)
# Carve circle
radius_cells = int(channel_width / cell_size / 2) + 1
# Curvature for Point Bar
# Calculate local curvature
# kappa = d(theta)/ds approx
if 0 < i < n_points-1:
dx_local = px[i+1] - px[i-1]
dy_local = py[i+1] - py[i-1]
# Vector along river: (dx, dy)
# Normal vector (Inside/Outside): (-dy, dx)
# Simple approach: Check neighbors
for dr in range(-radius_cells*3, radius_cells*3 + 1):
for dc in range(-radius_cells*3, radius_cells*3 + 1):
rr, cc = r_idx + dr, c_idx + dc
if 0 <= rr < rows and 0 <= cc < cols:
# Physical coord
gy = rr * cell_size
gx = cc * cell_size
dist = np.sqrt((gx - cx)**2 + (gy - cy)**2)
if dist < channel_width / 2:
# Channel Bed
grid.sediment[rr, cc] = 0 # Erode all sediment
grid.bedrock[rr, cc] = min(grid.bedrock[rr, cc], 50.0 - (gx/1000.0)*5.0 - channel_depth)
elif dist < channel_width / 2 + levee_width:
# Levee (Both sides initially)
grid.sediment[rr, cc] += levee_height * np.exp(-(dist - channel_width/2)/10.0)
# Point Bar Deposition: Inner Bend
# If turning LEFT, Inner is LEFT.
# Local curvature check required.
# Or just use pre-calc theta?
pass
# [Fix] To make it smooth, use diffusion
erosion = ErosionProcess(grid)
erosion.hillslope_diffusion(dt=1.0)
# [Fix] Water Depth
# Fill channel using HydroKernel (Physics Flow)
grid.update_elevation()
# Add flow source at start of path
# Find start point (min X)
start_idx = np.argmin(px)
sx, sy = px[start_idx], py[start_idx]
sr, sc = int(sy/cell_size), int(sx/cell_size)
precip = np.zeros((rows, cols))
if 0 <= sr < rows and 0 <= sc < cols:
precip[sr-2:sr+3, sc-2:sc+3] = 20.0 # Source
# Also some rain mapping to channel?
# Route flow
hydro = HydroKernel(grid)
discharge = hydro.route_flow_d8(precipitation=precip)
# Map to depth
water_depth = np.log1p(discharge) * 0.5
water_depth[water_depth < 0.1] = 0
# Calculate sinuosity for UI
path_len = np.sum(np.sqrt(np.diff(px)**2 + np.diff(py)**2))
straight = np.sqrt((px[-1]-px[0])**2 + (py[-1]-py[0])**2) + 0.01
sinuosity = path_len / straight
return {
'elevation': grid.elevation,
'water_depth': water_depth,
'sinuosity': sinuosity,
'oxbow_lakes': [] # TODO: Implement Oxbow in grid
}
@st.cache_data(ttl=3600)
def simulate_delta(theory: str, time_years: int, params: dict, grid_size: int = 100):
"""
์‚ผ๊ฐ์ฃผ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (Process-Based)
- ํ•˜์ฒœ์ด ๋ฐ”๋‹ค๋กœ ์œ ์ž… -> ์œ ์† ๊ฐ์†Œ -> ํ‡ด์  -> ํ•ด์•ˆ์„  ์ „์ง„(Progradation) -> ์œ ๋กœ ๋ณ€๊ฒฝ(Avulsion)
"""
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size, sea_level=0.0)
rows, cols = grid_size, grid_size
# 1. ์ดˆ๊ธฐ ์ง€ํ˜•
# Land (Top) -> Sea (Bottom)
# ์™„๋งŒํ•œ ๊ฒฝ์‚ฌ
center = cols // 2
# Bedrock Slope
# Row 0: +20m -> Row 100: -20m
Y, X = np.meshgrid(np.linspace(0, 1000, cols), np.linspace(0, 1000, rows))
grid.bedrock = 20.0 - (Y / 1000.0) * 40.0
# Pre-carve a slight valley upstream to guide initial flow
for r in range(rows // 3):
for c in range(cols):
dist = abs(c - center)
if dist < 10:
grid.bedrock[r, c] -= 2.0 * (1.0 - dist/10.0)
grid.update_elevation()
# 2. ๋ฌผ๋ฆฌ ์—”์ง„
hydro = HydroKernel(grid)
erosion = ErosionProcess(grid, K=0.02, m=1.0, n=1.0)
# ํŒŒ๋ผ๋ฏธํ„ฐ
river_flux = params.get('river', 0.5) * 200.0 # Sediment input
wave_energy = params.get('wave', 0.5)
# Delta Type Logic (Process-based modulation)
# Wave energy high -> Diffusion high -> Arcuate / Smooth Coast
# Wave energy low -> Diffusion low -> Bird's Foot
diffusion_rate = 0.01 + wave_energy * 0.1
steps = max(50, min(time_years // 100, 300))
dt = 1.0
# 3. ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฃจํ”„
for i in range(steps):
# ๊ฐ•์ˆ˜ (์ƒ๋ฅ˜ ์œ ์ž…)
precip = np.zeros((rows, cols))
precip[0:2, center-2:center+3] = 20.0
# Flow
discharge = hydro.route_flow_d8(precipitation=precip)
# Sediment Inflow at top
grid.sediment[0:2, center-2:center+3] += river_flux * 0.1 * dt
# Transport & Deposit
erosion.simulate_transport(discharge, dt=dt)
# Wave Action (Diffusion)
# ํ•ด์•ˆ์„  ๊ทผ์ฒ˜์—์„œ ํ™•์‚ฐ์ด ์ผ์–ด๋‚จ
# Hillslope diffusion approximates wave smoothing
erosion.hillslope_diffusion(dt=dt * diffusion_rate * 100.0)
grid.update_elevation()
# 4. ๊ฒฐ๊ณผ ์ •๋ฆฌ
# Water Depth Calculation
# Sea Depth (flat) vs River Depth (flow)
# Recalculate final flow
precip_final = np.zeros((rows, cols))
precip_final[0:2, center-2:center+3] = 10.0
discharge_final = hydro.route_flow_d8(precipitation=precip_final)
# 1. Sea Water
water_depth = np.zeros_like(grid.elevation)
sea_mask = grid.elevation < 0
water_depth[sea_mask] = -grid.elevation[sea_mask]
# 2. River Water
river_depth = np.log1p(discharge_final) * 0.5
land_mask = grid.elevation >= 0
# Combine (On land, show river. At sea, show sea depth)
water_depth[land_mask] = river_depth[land_mask]
# Calculate Metrics
# Area: Sediment accumulated above sea level (approx)
# Exclude initial land (bedrock > 0)
delta_mask = (grid.elevation > 0) & (grid.bedrock < 0)
area = np.sum(delta_mask) * (cell_size**2) / 1e6
# Determine Type for UI display
if wave_energy > 0.6:
delta_type = "์›ํ˜ธ์ƒ (Arcuate)"
elif river_flux > 300 and wave_energy < 0.3:
delta_type = "์กฐ์กฑ์ƒ (Bird's Foot)"
else:
delta_type = "ํ˜ผํ•ฉํ˜• (Mixed)"
return {'elevation': grid.elevation, 'water_depth': water_depth, 'area': area, 'delta_type': delta_type}
@st.cache_data(ttl=3600)
def simulate_coastal(theory: str, time_years: int, params: dict, grid_size: int = 100):
"""ํ•ด์•ˆ ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฌผ๋ฆฌ ์—”์ง„ ์ ์šฉ)"""
# 1. ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™” (Headland & Bay)
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
# ๊ธฐ๋ณธ: ๊นŠ์€ ๋ฐ”๋‹ค -> ์–•์€ ๋ฐ”๋‹ค -> ์œก์ง€ (Y์ถ• ๋ฐฉํ–ฅ)
for r in range(rows):
# Y=0(Deep Ocean) -> Y=100(Land)
base_elev = (r / rows) * 60.0 - 20.0 # -20m ~ +40m
grid.bedrock[r, :] = base_elev
# ๊ณถ (Headland) ๋Œ์ถœ
# ์ค‘์•™ ๋ถ€๋ถ„์€ ํ•ด์•ˆ์„ ์ด ๋ฐ”๋‹ค(Y=low) ์ชฝ์œผ๋กœ ํŠ€์–ด๋‚˜์˜ด
center = cols // 2
headland_width = cols // 3
for c in range(cols):
dist = abs(c - center)
if dist < headland_width:
# ๋Œ์ถœ๋ถ€ ์ถ”๊ฐ€ ๋†’์ด
protrusion = (1.0 - dist/headland_width) * 40.0
# ๋ฐ”๋‹ค ์ชฝ์œผ๋กœ ์—ฐ์žฅ
grid.bedrock[:, c] += protrusion * 0.5 # ์ „์ฒด์ ์œผ๋กœ ๋†’์ž„
# ์•ž๋ถ€๋ถ„์„ ๋” ๋ฐ”๋‹ค๋กœ
for r in range(rows):
if r < rows // 2: # ๋ฐ”๋‹ค ์ชฝ ์ ˆ๋ฐ˜
grid.bedrock[r, c] += protrusion * (1.0 - r/(rows//2))
# ๋žœ๋ค ๋…ธ์ด์ฆˆ
np.random.seed(42)
grid.bedrock += np.random.rand(rows, cols) * 2.0
grid.update_elevation()
# 2. ์—”์ง„
erosion = ErosionProcess(grid, K=0.01)
steps = 100
wave_height = params.get('wave_height', 2.0)
rock_resistance = params.get('rock_resistance', 0.5)
# ํŒŒ๋ž‘ ์—๋„ˆ์ง€ ๊ณ„์ˆ˜ (์•”์„ ์ €ํ•ญ ๋ฐ˜๋Œ€)
erodibility = (1.0 - rock_resistance) * 0.2
result_type = "ํ•ด์‹์•  & ํŒŒ์‹๋Œ€"
for i in range(steps):
# [Hybrid Approach]
# ๊ต๊ณผ์„œ์ ์ธ ํ•ด์‹์• (Sea Cliff)์™€ ํŒŒ์‹๋Œ€(Wave-cut Platform) ๊ฐ•์ œ
# 1. Retreat Cliff
# Amount of retreat proportional to step
retreat_dist = min(30, i * 0.5)
# Current Cliff Position (roughly)
# Original Headland was centered at Y=50 (approx)
# We push Y back based on X (Headland shape)
# Platform mask (Area eroded)
# Headland (center cols) retreats faster? No, wave focuses on headland.
# Define Cliff Line
for c in range(cols):
dist = abs(c - center)
if dist < headland_width:
# Original protrusion extent
orig_y = 50 + (1.0 - dist/headland_width) * 40.0
# Current cliff y (Retreating)
# fast retreat at tip
retreat_local = retreat_dist * (1.0 + (1.0 - dist/headland_width))
current_y = orig_y - retreat_local
current_y = max(current_y, 20.0) # Limit
# Apply Profile
# Platform (Low, flat) below current_y
# Cliff (Steep) at current_y
# Platform level: -10 ~ 0 approx
# Carve everything sea-side of current_y down to platform level
for r in range(rows):
if r < current_y:
# Platform
target_h = -5.0 + (r/100.0)*2.0
if grid.bedrock[r, c] > target_h:
grid.bedrock[r, c] = target_h
else:
# Cliff face or Land
# Keep heavy
pass
# 2. Physics detail (Stacks/Arches?)
# Leave some random columns (Stacks) on the platform
if i == steps - 1:
# Random Stacks
stack_prob = 0.02
noise = np.random.rand(rows, cols)
platform_mask = (grid.bedrock < 0) & (grid.bedrock > -10)
grid.bedrock[platform_mask & (noise < stack_prob)] += 30.0 # Stacks
result_type = "ํ•ด์‹์•  & ํŒŒ์‹๋Œ€ & ์‹œ์Šคํƒ"
return {
'elevation': grid.elevation,
'type': result_type,
'cliff_retreat': 0, 'platform_width': 0, 'notch_depth': 0
}
@st.cache_data(ttl=3600)
def simulate_coastal_deposition(theory: str, time_years: int, params: dict, grid_size: int = 100):
"""ํ•ด์•ˆ ํ‡ด์  ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - ์‚ฌ์ทจ, ์œก๊ณ„๋„, ๊ฐฏ๋ฒŒ"""
x = np.linspace(0, 1000, grid_size)
y = np.linspace(0, 1000, grid_size)
X, Y = np.meshgrid(x, y)
elevation = np.zeros((grid_size, grid_size))
dt = 100
steps = max(1, time_years // dt)
# ๊ณตํ†ต: ํ•ด์ˆ˜๋ฉด 0m ๊ธฐ์ค€
if theory == "spit":
# ์‚ฌ์ทจ & ์„ํ˜ธ: ๊บพ์ธ ํ•ด์•ˆ์„ ์—์„œ ๋ชจ๋ž˜๊ฐ€ ์—ฐ์žฅ๋จ
# ์ดˆ๊ธฐ ์ง€ํ˜•: ์™ผ์ชฝ์€ ์œก์ง€, ์˜ค๋ฅธ์ชฝ์€ ๋งŒ(Bay)
# ๋งŒ์˜ ์ž…๊ตฌ: X=300 ์ง€์ 
coast_y = 200
# ์œก์ง€ ๊ธฐ๋ณธ
land_mask = (X < 300) & (Y > coast_y) # ์™ผ์ชฝ ํ•ด์•ˆ
elevation[land_mask] = 10
# ๋งŒ์˜ ์•ˆ์ชฝ (์˜ค๋ฅธ์ชฝ ๊นŠ์ˆ™ํ•œ ๊ณณ)
bay_coast_y = 600
bay_mask = (X >= 300) & (Y > bay_coast_y)
elevation[bay_mask] = 10
# ๋ฐ”๋‹ค (์ ์ง„์  ๊นŠ์–ด์ง)
sea_mask = elevation == 0
elevation[sea_mask] = -10 - (Y[sea_mask]/1000)*10
# ์‚ฌ์ทจ ์„ฑ์žฅ (์™ผ์ชฝ ๊ณถ์—์„œ ์˜ค๋ฅธ์ชฝ์œผ๋กœ)
growth_rate = params.get('drift_strength', 0.5) * 5
spit_len = min(600, steps * growth_rate)
# ์‚ฌ์ทจ ํ˜•์„ฑ (X: 300 -> 300+len)
spit_width = 30 + params.get('sand_supply', 0.5) * 20
spit_mask = (X >= 300) & (X < 300 + spit_len) & (Y > coast_y - spit_width/2) & (Y < coast_y + spit_width/2)
# ๋๋ถ€๋ถ„์€ ๋ญ‰ํˆญํ•˜๊ฒŒ/ํœ˜์–ด์ง€๊ฒŒ (Hook)
if spit_len > 100:
hook_x = 300 + spit_len
hook_mask = (X > hook_x - 50) & (X < hook_x) & (Y > coast_y) & (Y < coast_y + 100)
# ํŒŒํ–ฅ์— ๋”ฐ๋ผ ํœ˜์–ด์ง
if params.get('wave_angle', 45) > 30:
elevation[hook_mask & (elevation < 0)] = 2
elevation[spit_mask] = 3 # ํ•ด์ˆ˜๋ฉด ์œ„๋กœ ๋“œ๋Ÿฌ๋‚จ
# ์„ํ˜ธ ํ˜•์„ฑ ์—ฌ๋ถ€ (์‚ฌ์ทจ๊ฐ€ ๋งŒ์„ ๋ง‰์•˜๋Š”์ง€)
lagoon_closed = spit_len > 600
result_type = "์‚ฌ์ทจ (Spit)"
if lagoon_closed: result_type += " & ์„ํ˜ธ (Lagoon)"
elif theory == "tombolo":
# ์œก๊ณ„๋„: ์œก์ง€ + ์„ฌ + ์‚ฌ์ฃผ
coast_y = 200
# ์œก์ง€
elevation[Y < coast_y] = 10
elevation[Y >= coast_y] = -15 # ๋ฐ”๋‹ค
# ์„ฌ (์ค‘์•™์— ์œ„์น˜)
island_dist = 300 + params.get('island_dist', 0.5) * 300 # 300~600m
island_y = coast_y + island_dist
island_r = 80 + params.get('island_size', 0.5) * 50
dist_from_island = np.sqrt((X-500)**2 + (Y-island_y)**2)
island_mask = dist_from_island < island_r
elevation[island_mask] = 30 * np.exp(-dist_from_island[island_mask]**2 / (island_r/2)**2)
# ์œก๊ณ„์‚ฌ์ฃผ (Tombolo) ์„ฑ์žฅ
# ํŒŒ๋ž‘์ด ์„ฌ ๋’ค์ชฝ์œผ๋กœ ํšŒ์ ˆ๋˜์–ด ํ‡ด์ 
# ์œก์ง€(200)์™€ ์„ฌ(island_y) ์‚ฌ์ด ์ด์–ด์ง
connect_factor = min(1.0, steps * params.get('wave_energy', 0.5) * 0.05)
# ๋ชจ๋ž˜ํ†ฑ (X=500 ์ค‘์‹ฌ)
bar_width = 40 + connect_factor * 100
bar_mask = (X > 500 - bar_width/2) & (X < 500 + bar_width/2) & (Y >= coast_y) & (Y <= island_y)
# ๋ชจ๋ž˜ํ†ฑ ๋†’์ด: ์„œ์„œํžˆ ์˜ฌ๋ผ์˜ด
target_height = 2 # ํ•ด์ˆ˜๋ฉด๋ณด๋‹ค ์•ฝ๊ฐ„ ๋†’์Œ
current_bar_h = -5 + connect_factor * 7
elevation[bar_mask] = np.maximum(elevation[bar_mask], current_bar_h)
result_type = "์œก๊ณ„๋„ (Tombolo)" if current_bar_h > 0 else "์œก๊ณ„์‚ฌ์ฃผ ํ˜•์„ฑ ์ค‘"
elif theory == "tidal_flat":
# ๊ฐฏ๋ฒŒ: ์™„๋งŒํ•œ ๊ฒฝ์‚ฌ + ์กฐ์ˆ˜ ๊ณจ (Tidal Creek)
# ๋งค์šฐ ์™„๋งŒํ•œ ๊ฒฝ์‚ฌ
slope = 0.005
elevation = 5 - Y * slope # Y=0: 5m -> Y=1000: 0m ...
# ์กฐ์ฐจ (Tidal Range)
tidal_range = params.get('tidal_range', 3.0) # 0.5 ~ 6m
high_tide = tidal_range / 2
low_tide = -tidal_range / 2
# ๊ฐฏ๋ฒŒ ์˜์—ญ: High Tide์™€ Low Tide ์‚ฌ์ด
flat_mask = (elevation < high_tide) & (elevation > low_tide)
# ๊ฐฏ๋ฒŒ ๊ณจ (Meandering Creeks)
# ํ”„๋ž™ํƒˆ ์ˆ˜๋กœ
n_creeks = 3
for i in range(n_creeks):
cx = 200 + i * 300
cy = np.linspace(200, 1000, 200)
# ์ˆ˜๋กœ ๊ตด๊ณก
cx_curve = cx + 50 * np.sin(cy * 0.02) + np.random.normal(0, 5, 200)
for j, y_pos in enumerate(cy):
iy = int(y_pos * grid_size / 1000)
ix = int(cx_curve[j] * grid_size / 1000)
if 0 <= iy < grid_size and 0 <= ix < grid_size:
# ์ˆ˜๋กœ ๊นŠ์ด
depth = 2 + (y_pos/1000) * 3 # ๋ฐ”๋‹ค ์ชฝ์œผ๋กœ ๊ฐˆ์ˆ˜๋ก ๊นŠ์–ด์ง
elevation[iy, max(0,ix-3):min(grid_size,ix+4)] -= depth
result_type = "๊ฐฏ๋ฒŒ (Tidal Flat)"
else:
result_type = "ํ•ด์•ˆ ์ง€ํ˜•"
return {
'elevation': elevation,
'type': result_type,
'cliff_retreat': 0, 'platform_width': 0, 'notch_depth': 0
}
@st.cache_data(ttl=3600)
def simulate_alluvial_fan(time_years: int, params: dict, grid_size: int = 100):
"""
์„ ์ƒ์ง€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (Project Genesis Unified Engine)
- ํ†ตํ•ฉ ์—”์ง„(EarthSystem)์„ ์‚ฌ์šฉํ•˜์—ฌ ์ž์—ฐ์Šค๋Ÿฌ์šด ์„ ์ƒ์ง€ ํ˜•์„ฑ ๊ณผ์ • ์žฌํ˜„
- ์ƒ๋ฅ˜ ์‚ฐ์ง€ -> ๊ธ‰๊ฒฝ์‚ฌ ๋ณ€ํ™˜๋ถ€(Apex) -> ํ‰์ง€ ํ™•์‚ฐ
"""
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
center = cols // 2
apex_row = int(rows * 0.2)
# 1. ์ดˆ๊ธฐ ์ง€ํ˜• ์„ค์ • (Scenario Setup)
# A. Mountain Zone (Steep)
for r in range(apex_row):
# 100m -> 50m drop
grid.bedrock[r, :] = 100.0 - (r / apex_row) * 50.0
# B. Plain Zone (Flat)
# 50m -> 40m (Very gentle slope)
for r in range(apex_row, rows):
grid.bedrock[r, :] = 50.0 - ((r - apex_row) / (rows - apex_row)) * 10.0
# C. Canyon Carving (Channel in Mountain)
for r in range(apex_row + 5): # Extend slightly beyond apex
for c in range(cols):
dist = abs(c - center)
width = 3 + (r / apex_row) * 5
if dist < width:
# V-shape cut
depth = 10.0 * (1.0 - dist/width)
grid.bedrock[r, c] -= depth
# Add random noise
np.random.seed(42)
grid.bedrock += np.random.rand(rows, cols) * 1.0
grid.update_elevation()
# 2. ํ†ตํ•ฉ ์—”์ง„ ์ดˆ๊ธฐํ™” (Unified Engine)
engine = EarthSystem(grid)
# 3. ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์„ค์ • (Config)
# K๊ฐ’์„ ๋‚ฎ์ถฐ์„œ ์šด๋ฐ˜ ๋Šฅ๋ ฅ(Capacity)์„ ์ค„์ž„ -> ํ‰์ง€์—์„œ ํ‡ด์  ์œ ๋„
engine.erosion.K = 0.005
steps = max(50, min(time_years // 100, 200))
sediment_supply = params.get('sediment', 0.5) * 1000.0 # ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰๋Ÿ‰ ๋Œ€ํญ ์ฆ๊ฐ€
# Settings for the Engine
settings = {
'precipitation': 0.0,
'rain_source': (0, center, 5, 50.0), # ๊ฐ•์ˆ˜๋Ÿ‰ ์ฆ๊ฐ€
'sediment_source': (apex_row, center, 2, sediment_supply),
'diffusion_rate': 0.1 # ํ™•์‚ฐ ํ™œ์„ฑํ™” (๋ถ€์ฑ„๊ผด ํ˜•์„ฑ ๋„์›€)
}
# 4. ์—”์ง„ ๊ตฌ๋™ (The Loop)
for i in range(steps):
engine.step(dt=1.0, settings=settings)
# 5. ๊ฒฐ๊ณผ ๋ฐ˜ํ™˜
engine.get_state() # Update grid state one last time
# Calculate metrics
fan_mask = grid.sediment > 1.0
area = np.sum(fan_mask) * (cell_size**2) / 1e6
radius = np.sqrt(area * 1e6 / np.pi) * 2 if area > 0 else 0
# Debug Info
sed_max = grid.sediment.max()
return {
'elevation': grid.elevation,
'water_depth': grid.water_depth,
'sediment': grid.sediment, # Explicit return for visualization
'area': area,
'radius': radius,
'debug_sed_max': sed_max,
'debug_steps': steps
}
@st.cache_data(ttl=3600)
def simulate_river_terrace(time_years: int, params: dict, grid_size: int = 100):
"""ํ•˜์•ˆ๋‹จ๊ตฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฌผ๋ฆฌ ์—”์ง„ ์ ์šฉ)"""
# 1. ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™” (V์ž๊ณก ์œ ์‚ฌ)
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
center = cols // 2
# ์ดˆ๊ธฐ: ๋„“์€ ๋ฒ”๋žŒ์›์ด ์žˆ๋Š” U์ž๊ณก ํ˜•ํƒœ
for r in range(rows):
grid.bedrock[r, :] = 150.0 - (r/rows)*20.0 # ์™„๋งŒํ•œ ํ•˜๋ฅ˜ ๊ฒฝ์‚ฌ
for c in range(cols):
dist = abs(c - center)
# ๋„“์€ ํ•˜๊ณก (200m)
if dist < 100:
grid.bedrock[:, c] -= 20.0
else:
# ์–‘์ชฝ ์–ธ๋•
grid.bedrock[:, c] += (dist - 100) * 0.2
grid.update_elevation()
hydro = HydroKernel(grid)
erosion = ErosionProcess(grid, K=0.001)
uplift_rate = params.get('uplift', 0.5) * 0.1 # ์œต๊ธฐ ์†๋„
n_terraces = int(params.get('n_terraces', 3))
# ์‚ฌ์ดํด ๊ณ„์‚ฐ
# ํ‰ํ˜• ์ƒํƒœ(๋ฒ”๋žŒ์› ํ˜•์„ฑ) -> ์œต๊ธฐ(ํ•˜๊ฐ) -> ํ‰ํ˜•(์ƒˆ ๋ฒ”๋žŒ์›)
total_cycles = n_terraces
current_time = 0
terrace_heights = []
# [Optimization] Performance Cap
# Avoid excessive loops if time_years is large
raw_duration = max(20, time_years // total_cycles)
max_duration_per_cycle = 50 # Fixed physics steps per cycle
# Scale physics parameters to match time scaling
time_scale = raw_duration / max_duration_per_cycle
dt = 1.0 * time_scale # Increase time step
# [Hybrid Approach]
# ๊ต๊ณผ์„œ์ ์ธ ํ•˜์•ˆ๋‹จ๊ตฌ(Stairs) ํ˜•ํƒœ ๊ฐ•์ œ + ์• ๋‹ˆ๋ฉ”์ด์…˜ ์ง€์›
# 1. Base U-Valley (Already initialized)
# 2. Determine Progress based on Time
# Assume 1 Terrace takes 20,000 years to form fully (Uplift + Incision)
years_per_cycle = 20000
# Calculate how many cycles are completed at current time
cycle_progress_float = time_years / years_per_cycle
completed_cycles = int(cycle_progress_float)
current_fraction = cycle_progress_float - completed_cycles
# Cap at n_terraces
if completed_cycles > n_terraces:
completed_cycles = n_terraces
current_fraction = 0.0
if completed_cycles == n_terraces:
current_fraction = 0.0 # Fully done
level_step = 20.0
# 3. Simulate Logic
# Run fully completed cycles first
for cycle in range(completed_cycles):
if cycle >= n_terraces: break
# A. Uplift (Full)
grid.bedrock += 10.0 * uplift_rate
# B. Incision (Full)
current_width = 100 - cycle * 20
for c in range(cols):
dist = abs(c - center)
if dist < current_width:
grid.bedrock[:, c] -= 15.0
# Record height
mid_elev = grid.bedrock[rows//2, center]
terrace_heights.append(mid_elev)
# Run current partial cycle (Animation effect)
if completed_cycles < n_terraces:
cycle = completed_cycles
# A. Partial Uplift
# Uplift happens gradually or triggered?
# Let's say Uplift scales with fraction
grid.bedrock += 10.0 * uplift_rate * current_fraction
# B. Partial Incision (Depth or Width?)
# Incision depth scales with fraction
current_width = 100 - cycle * 20
incision_depth = 15.0 * current_fraction
for c in range(cols):
dist = abs(c - center)
if dist < current_width:
grid.bedrock[:, c] -= incision_depth
# C. Smoothing (Physics Texture)
erosion.hillslope_diffusion(dt=5.0)
# [Fix] Water Depth
water_depth = np.zeros_like(grid.elevation)
center_c = cols // 2
# Determine current river width at bottom
# Just use a visual width
river_w = 10
water_depth[:, center_c-river_w:center_c+river_w] = 5.0
return {'elevation': grid.elevation, 'n_terraces': n_terraces, 'heights': terrace_heights, 'water_depth': water_depth}
@st.cache_data(ttl=3600)
def simulate_stream_piracy(time_years: int, params: dict, grid_size: int = 100):
"""ํ•˜์ฒœ์Ÿํƒˆ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - ๊ต๊ณผ์„œ์  ์ด์ƒ์  ๋ชจ์Šต"""
x = np.linspace(0, 1000, grid_size)
y = np.linspace(0, 1000, grid_size)
X, Y = np.meshgrid(x, y)
# ๊ธฐ๋ณธ ์ง€ํ˜•: ๊ฒฝ์‚ฌ๋ฉด (์ƒ๋ฅ˜๊ฐ€ ๋†’์Œ)
elevation = 150 - Y * 0.1
# ๋ถ„์ˆ˜๋ น (๋‘ ํ•˜์ฒœ ์‚ฌ์ด์˜ ๋Šฅ์„ )
ridge_x = 500
ridge = 20 * np.exp(-((X - ridge_x)**2) / (80**2))
elevation += ridge
# ํ•˜์ฒœ ๊ณ„๊ณก ํ˜•์„ฑ
# ํ”ผํƒˆํ•˜์ฒœ (์ขŒ์ธก, ์•ฝํ•œ ์นจ์‹๋ ฅ) - Y๋ฐฉํ–ฅ์œผ๋กœ ํ๋ฆ„
river1_x = 300
river1_valley = 30 * np.exp(-((X - river1_x)**2) / (40**2))
elevation -= river1_valley
# ์Ÿํƒˆํ•˜์ฒœ (์šฐ์ธก, ๊ฐ•ํ•œ ์นจ์‹๋ ฅ) - ๋” ๊นŠ์€ ๊ณ„๊ณก
river2_x = 700
erosion_diff = params.get('erosion_diff', 0.7)
river2_depth = 50 * erosion_diff
river2_valley = river2_depth * np.exp(-((X - river2_x)**2) / (50**2))
elevation -= river2_valley
dt = 100
steps = max(1, time_years // dt)
captured = False
capture_time = 0
elbow_point = None
# ๋‘๋ถ€์นจ์‹ ์ง„ํ–‰ (Process Visualization)
headcut_progress = min(steps * erosion_diff * 3, 200) # ์ตœ๋Œ€ 200m ์ง„ํ–‰
# ์Ÿํƒˆ ์ „์ด๋ผ๋„ ์นจ์‹๊ณก์ด ๋ถ„์ˆ˜๋ น ์ชฝ์œผ๋กœ ํŒŒ๊ณ ๋“œ๋Š” ๊ณผ์ • ์‹œ๊ฐํ™”
# ์Ÿํƒˆํ•˜์ฒœ(river2)์—์„œ ๋ถ„์ˆ˜๋ น(ridge) ์ชฝ์œผ๋กœ ์นจ์‹ ์ง„ํ–‰
# River2 X=700 -> Ridge X=500. Headcut moves Left.
current_head_x = river2_x - headcut_progress # 700 - progress
# ์นจ์‹ ์ฑ„๋„ ์ƒ์„ฑ (Progressive Channel)
# 700์—์„œ current_head_x๊นŒ์ง€ ํŒŒ๋ƒ„
if headcut_progress > 0:
# Y ์œ„์น˜๋Š” 400 (elbow_point ์˜ˆ์ •์ง€)
erosion_y = 400
# X range: current_head_x ~ 700
# Grid iterate or vector ops? Vector ops easier.
# Create a channel mask
channel_len = headcut_progress
# Gaussian profile along Y, Linear along X?
# X: current_head_x to 700
# We carve a path
eroding_mask_x = (X > current_head_x) & (X < 700)
eroding_mask_y = np.abs(Y - erosion_y) < 30
# Depth tapers at the head
dist_from_start = (700 - X)
depth_profile = river2_depth * 0.8 # Base depth
# Apply erosion
mask = eroding_mask_x & eroding_mask_y
elevation[mask] -= depth_profile * np.exp(-(Y[mask]-erosion_y)**2 / 20**2)
if headcut_progress > 150: # ๋ถ„์ˆ˜๋ น์„ ๋„˜์–ด ์Ÿํƒˆ ๋ฐœ์ƒ (150m is dist to ridge zone)
captured = True
capture_time = int(150 / (erosion_diff * 3) * dt)
elbow_point = (ridge_x - 50, 400) # ๊ตด๊ณก์  ์œ„์น˜
# ์Ÿํƒˆ ํ›„ ์ง€ํ˜• ๋ณ€ํ™” (์™„์ „ ์—ฐ๊ฒฐ)
# 1. ์Ÿํƒˆํ•˜์ฒœ์ด ๋ถ„์ˆ˜๋ น์„ ํŒŒ๊ณ  ํ”ผํƒˆํ•˜์ฒœ ์ƒ๋ฅ˜์™€ ์—ฐ๊ฒฐ
# Already partially done by progressive erosion, but let's connect fully
capture_zone_x = np.linspace(river1_x, current_head_x, 50) # Connect remaining gap
capture_zone_y = 400
for cx in capture_zone_x:
mask = ((X - cx)**2 + (Y - capture_zone_y)**2) < 30**2
elevation[mask] -= river2_depth * 0.8
# 2. ํ”ผํƒˆํ•˜์ฒœ ์ƒ๋ฅ˜ โ†’ ์Ÿํƒˆํ•˜์ฒœ์œผ๋กœ ์œ ์ž… (์ง๊ฐ ๊ตด๊ณก)
for j in range(grid_size):
if Y[j, 0] < capture_zone_y: # ์ƒ๋ฅ˜ ๋ถ€๋ถ„
# ํ”ผํƒˆํ•˜์ฒœ ์ƒ๋ฅ˜๋Š” ๊ทธ๋Œ€๋กœ
pass
else: # ํ•˜๋ฅ˜ ๋ถ€๋ถ„ - ์œ ๋Ÿ‰ ๊ฐ์†Œ๋กœ ์–•์•„์ง
mask = np.abs(X[j, :] - river1_x) < 40
elevation[j, mask] += 15 # ํ’๊ฐญ ํ˜•์„ฑ (๊ฑด์ฒœํ™”)
# 3. ํ’๊ฐญ ํ‘œ์‹œ (๋งˆ๋ฅธ ๊ณ„๊ณก)
wind_gap_y = capture_zone_y + 50
wind_gap_mask = (np.abs(X - river1_x) < 30) & (np.abs(Y - wind_gap_y) < 50)
elevation[wind_gap_mask] = elevation[wind_gap_mask].mean() # ํ‰ํƒ„ํ™”
# [Fix] Water Depth Calculation for Visualization
water_depth = np.zeros_like(elevation)
# 1. River 2 (Capturing Stream) - Always flowing
# Valley mask
# X > 550, Y > 0. Roughly.
# Actually use analytic distance check
dist_r2 = np.abs(X - river2_x)
# Head ward erosion channel
head_mask = (X > current_head_x) & (X < 700) & (np.abs(Y - 400) < 20)
r2_mask = (dist_r2 < 40) | head_mask
water_depth[r2_mask] = 3.0 # Deep water
# 2. River 1 (Victim Stream)
if not captured:
# Full flow
dist_r1 = np.abs(X - river1_x)
r1_mask = dist_r1 < 30
water_depth[r1_mask] = 3.0
else:
# Captured!
capture_y = 400
# Upstream (Y < capture_y) -> Flows to River 2
# Connect to R2
dist_r1_upper = np.abs(X - river1_x)
r1_upper_mask = (dist_r1_upper < 30) & (Y < capture_y)
water_depth[r1_upper_mask] = 3.0
# Connection channel
conn_mask = (X > river1_x) & (X < current_head_x) & (np.abs(Y - capture_y) < 20)
water_depth[conn_mask] = 3.0
# Downstream (Y > capture_y) -> Dry (Wind Gap)
# Maybe small misfit stream?
dist_r1_lower = np.abs(X - river1_x)
r1_lower_mask = (dist_r1_lower < 20) & (Y > capture_y + 50) # Skip wind gap
water_depth[r1_lower_mask] = 0.5 # Misfit stream (shallow)
return {
'elevation': elevation,
'captured': captured,
'capture_time': capture_time if captured else None,
'elbow_point': elbow_point,
'water_depth': water_depth
}
@st.cache_data(ttl=3600)
def simulate_entrenched_meander(time_years: int, params: dict, grid_size: int = 100):
"""
๊ฐ์ž… ๊ณก๋ฅ˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (Process-Based)
- Kinoshita Curve๋กœ ๊ณก๋ฅ˜ ํ˜•์„ฑ -> ์ง€๋ฐ˜ ์œต๊ธฐ -> ํ•˜๋ฐฉ ์นจ์‹(Incision)
"""
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
# 1. ์ดˆ๊ธฐ ์ง€ํ˜• ๋ฐ ๊ฒฝ๋กœ ์ƒ์„ฑ (Kinoshita Curve - simulate_meander์™€ ๋™์ผ ๋กœ์ง)
# ์œต๊ธฐ ์ „์˜ ํ‰ํƒ„ํ•œ ๋ฒ”๋žŒ์›
grid.bedrock[:] = 50.0
# Kinoshita Path Generation
n_points = 1000
s = np.linspace(0, 20, n_points)
# ์„ฑ์ˆ™ํ•œ ๊ณก๋ฅ˜ (High amplitude)
theta_0 = 1.8
flattening = 0.2
theta = theta_0 * np.sin(s) + (theta_0 * flattening) * np.sin(3 * s)
dx = np.cos(theta)
dy = np.sin(theta)
x = np.cumsum(dx)
y = np.cumsum(dy)
# Rotate & Scale
angle = np.arctan2(y[-1] - y[0], x[-1] - x[0])
rot_mat = np.array([[np.cos(-angle), -np.sin(-angle)],[np.sin(-angle), np.cos(-angle)]])
coords = np.vstack([x, y])
rotated = rot_mat @ coords
px = rotated[0, :]
py = rotated[1, :]
# Normalize
margin = 100
p_width = px.max() - px.min()
if p_width > 0:
scale = (1000 - 2*margin) / p_width
px = (px - px.min()) * scale + margin
py = py * scale
py = py - py.mean() + 500
# Slope terrain along X (since we rotated current to X-axis in Kinoshita logic above)
# Check px direction. px increases index 0->end.
# So Flow is West -> East (Left -> Right).
# Add Slope W->E
Y, X = np.meshgrid(np.linspace(0, 1000, rows), np.linspace(0, 1000, cols))
grid.bedrock[:] = 50.0 - (X / 1000.0) * 10.0 # 10m drop
# 2. ํ•˜์ฒœ ๊ฒฝ๋กœ ๋งˆ์Šคํฌ ์ƒ์„ฑ
river_mask = np.zeros((rows, cols), dtype=bool)
channel_width = 30.0 # m
# Draw channel
# Pre-calculate cells in channel to speed up loop
for k in range(n_points):
cx, cy = px[k], py[k]
c_idx = int(cx / cell_size)
r_idx = int(cy / cell_size)
radius_cells = int(channel_width / cell_size / 2) + 1
for dr in range(-radius_cells, radius_cells + 1):
for dc in range(-radius_cells, radius_cells + 1):
rr, cc = r_idx + dr, c_idx + dc
if 0 <= rr < rows and 0 <= cc < cols:
dist = np.sqrt((rr*cell_size - cy)**2 + (cc*cell_size - cx)**2)
if dist < channel_width/2:
river_mask[rr, cc] = True
# 3. ์œต๊ธฐ ๋ฐ ์นจ์‹ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
uplift_rate = params.get('uplift', 0.5) * 0.01 # m/year -> scale down for sim step
incision_power = 1.2 # ์นจ์‹๋ ฅ์ด ์œต๊ธฐ๋ณด๋‹ค ๊ฐ•ํ•ด์•ผ ํŒŒ์ž„
steps = max(50, min(time_years // 100, 300))
dt = 10.0
incision_type = params.get('incision_type', 'U') # U (Ingrown) or V (Entrenched)
for i in range(steps):
# Uplift entire terrain
grid.bedrock += uplift_rate * dt
# Maintain slope? Uplift is uniform. Slope is preserved.
# Channel Incision (Erosion)
current_incision = uplift_rate * dt * incision_power
# Apply incision to channel
grid.bedrock[river_mask] -= current_incision
# Slope Evolution (Diffusion)
diff_k = 0.01 if incision_type == 'V' else 0.05
grid.update_elevation()
erosion = ErosionProcess(grid)
erosion.hillslope_diffusion(dt=dt * diff_k)
# 4. ๊ฒฐ๊ณผ ์ •๋ฆฌ
grid.update_elevation()
# Calculate depth
max_elev = grid.elevation.max()
min_elev = grid.elevation[river_mask].mean()
depth = max_elev - min_elev
type_name = "์ฐฉ๊ทผ ๊ณก๋ฅ˜ (Ingrown)" if incision_type == 'U' else "๊ฐ์ž… ๊ณก๋ฅ˜ (Entrenched)"
# [Fix] Water Depth using HydroKernel
# Add source at left
precip = np.zeros((rows, cols))
# Find start
start_idx = np.argmin(px)
sx, sy = px[start_idx], py[start_idx]
sr, sc = int(sy/cell_size), int(sx/cell_size)
if 0 <= sr < rows and 0 <= sc < cols:
precip[sr-2:sr+3, sc-2:sc+3] = 50.0
hydro = HydroKernel(grid)
discharge = hydro.route_flow_d8(precipitation=precip)
water_depth = np.log1p(discharge) * 0.5
water_depth[water_depth < 0.1] = 0
return {'elevation': grid.elevation, 'depth': depth, 'type': type_name, 'water_depth': water_depth}
@st.cache_data(ttl=3600)
def simulate_waterfall(time_years: int, params: dict, grid_size: int = 100):
"""
ํญํฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (Process-Based)
- ๋‘๋ถ€ ์นจ์‹(Headward Erosion) ์›๋ฆฌ ๊ตฌํ˜„
- ๊ธ‰๊ฒฝ์‚ฌ(ํญํฌ) -> ๊ฐ•ํ•œ ์ „๋‹จ๋ ฅ -> ์นจ์‹ -> ์ƒ๋ฅ˜๋กœ ํ›„ํ‡ด
"""
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
# 1. ์ดˆ๊ธฐ ์ง€ํ˜•: ๋‹จ๋‹จํ•œ ๊ธฐ๋ฐ˜์•” ์ ˆ๋ฒฝ
center = cols // 2
# ์ƒ๋ฅ˜ (100m) -> ํ•˜๋ฅ˜ (0m)
# ์ ˆ๋ฒฝ ์œ„์น˜: ์ค‘์•™
cliff_pos = 500
Y, X = np.meshgrid(np.linspace(0, 1000, rows), np.linspace(0, 1000, cols))
grid.bedrock[:] = 100.0
grid.bedrock[Y >= cliff_pos] = 20.0 # Downstream base level
# Slope face
slope_mask = (Y >= cliff_pos-20) & (Y < cliff_pos+20)
# Linear ramp for stability initially
grid.bedrock[slope_mask] = 100.0 - (Y[slope_mask] - (cliff_pos-20))/40.0 * 80.0
# Pre-carve channel to guide water
grid.bedrock[:, center-5:center+5] -= 2.0
grid.update_elevation()
# 2. ๋ฌผ๋ฆฌ ํŒฉํ„ฐ
hydro = HydroKernel(grid)
erosion = ErosionProcess(grid, K=0.1) # K very high for noticeable retreat
retreat_k = params.get('retreat_rate', 0.5) * 5.0 # retreat multiplier
steps = max(50, min(time_years // 100, 300))
dt = 1.0
# Track position
initial_knickpoint = cliff_pos
current_knickpoint = cliff_pos
for i in range(steps):
# Flow
precip = np.zeros((rows, cols))
precip[0:5, center-5:center+5] = 20.0 # Upstream flow source
discharge = hydro.route_flow_d8(precipitation=precip)
# Erosion (Stream Power)
# E = K * A^m * S^n
# Waterfall face has huge S -> Huge E
# To simulate retreat, we need significant erosion at the knickpoint
# We modify K locally based on params
# Or just let standard Stream Power do it?
# Standard SP might smooth the slope rather than maintain a cliff.
# "Parallel Retreat" requires a cap rock mechanism (hard layer over soft layer).
# Let's simulate Cap Rock simple logic:
# Erosion only effective if slope > critical
# Calculate Slope (Magnitude)
grad_y, grad_x = np.gradient(grid.elevation)
slope = np.sqrt(grad_y**2 + grad_x**2)
# Enhanced erosion at steep slopes (Face)
cliff_mask = slope > 0.1
# Apply extra erosion to cliff face to simulate undercutting/retreat
# Erosion proportional to water flux * slope
# K_eff = K * retreat_k
eroded_depth = discharge * slope * retreat_k * dt * 0.05
grid.bedrock[cliff_mask] -= eroded_depth[cliff_mask]
# Flattening prevention (maintain cliff)
# If lower part erodes, upper part becomes unstable -> discrete collapse
# Simple simulation: Smoothing? No, simplified retreat
# Just pure erosion usually rounds it.
# Let's rely on the high K on the face.
grid.update_elevation()
erosion.hillslope_diffusion(dt=dt*0.1) # Minimal diffusion to keep sharpness
# 3. ๊ฒฐ๊ณผ ๋ถ„์„
# ์นจ์‹์ด ๊ฐ€์žฅ ๋งŽ์ด ์ผ์–ด๋‚œ ์ง€์  ์ฐพ๊ธฐ (Steepest slope upstream)
grad_y, grad_x = np.gradient(grid.elevation)
slope = np.sqrt(grad_y**2 + grad_x**2)
# Find max slope index along river profile
profile_slope = slope[:, center]
# Find the peak slope closest to upstream
peaks = np.where(profile_slope > 0.05)[0]
if len(peaks) > 0:
current_knickpoint = peaks.min() * cell_size
else:
current_knickpoint = 1000 # Eroded away?
retreat_amount = current_knickpoint - initial_knickpoint # Should be negative (moves up = smaller Y)
# But wait, Y increases downstream?
# Y=0 (Upstream), Y=1000 (Downstream).
# Cliff at 500. Upstream is 0-500.
# Retreat means moving towards 0.
# So current should be < 500.
total_retreat = abs(500 - current_knickpoint)
# [Fix] Water Depth
precip = np.zeros((rows, cols))
precip[0:5, center-5:center+5] = 10.0
discharge = hydro.route_flow_d8(precipitation=precip)
water_depth = np.log1p(discharge) * 0.5
# Plunge pool depth?
# Add fake pool depth if slope is high
water_depth[slope > 0.1] += 2.0
return {'elevation': grid.elevation, 'retreat': total_retreat, 'water_depth': water_depth}
@st.cache_data(ttl=3600)
def simulate_braided_stream(time_years: int, params: dict, grid_size: int = 100):
"""๋ง์ƒ ํ•˜์ฒœ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฌผ๋ฆฌ ์—”์ง„ ์ ์šฉ)"""
# 1. ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™”
# ๋„“๊ณ  ํ‰ํƒ„ํ•œ ํ•˜๊ณก
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
# ๊ธฐ๋ณธ ๊ฒฝ์‚ฌ (๋ถ -> ๋‚จ)
for r in range(rows):
grid.bedrock[r, :] = 100.0 - (r / rows) * 10.0 # 100m -> 90m (์™„๊ฒฝ์‚ฌ)
# ํ•˜๊ณก (Valley) ํ˜•์„ฑ - ์–‘์ชฝ์ด ๋†’์Œ
center = cols // 2
for c in range(cols):
dist = abs(c - center)
# 800m ํญ์˜ ๋„“์€ ๊ณ„๊ณก
if dist > 20:
grid.bedrock[:, c] += (dist - 20) * 0.5
# ๋žœ๋ค ๋…ธ์ด์ฆˆ (์œ ๋กœ ํ˜•์„ฑ์„ ์œ„ํ•œ ๋ถˆ๊ทœ์น™์„ฑ)
np.random.seed(42)
grid.bedrock += np.random.rand(rows, cols) * 1.5
grid.update_elevation()
# 2. ์—”์ง„
hydro = HydroKernel(grid)
erosion = ErosionProcess(grid, K=0.05, m=1.0, n=1.0) # K Increased
# ํŒŒ๋ผ๋ฏธํ„ฐ
n_channels = int(params.get('n_channels', 5)) # ์ž…๋ ฅ ์œ ๋Ÿ‰์˜ ๋ถ„์‚ฐ ์ •๋„?
sediment_load = params.get('sediment', 0.5) * 200.0 # ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰๋Ÿ‰
dt = 1.0
steps = 100
for i in range(steps):
# ๋ณ€๋™ํ•˜๋Š” ์œ ๋Ÿ‰ (Braiding ์œ ๋ฐœ)
# ์‹œ๊ฐ„/๊ณต๊ฐ„์ ์œผ๋กœ ๋ณ€ํ•˜๋Š” ๊ฐ•์ˆ˜
precip = np.random.rand(rows, cols) * 0.1 + 0.01 # Noise Increased
discharge = hydro.route_flow_d8(precipitation=precip)
# ์ƒ๋ฅ˜ ์œ ์ž… (ํ‡ด์ ๋ฌผ ๊ณผ๋ถ€ํ•˜)
# ์ƒ๋ฅ˜ ์ค‘์•™๋ถ€์— ๋ฌผ๊ณผ ํ‡ด์ ๋ฌผ ์Ÿ์•„๋ถ€์Œ
inflow_width = max(3, n_channels * 2)
grid.sediment[0:2, center-inflow_width:center+inflow_width] += sediment_load * dt * 0.1
discharge[0:2, center-inflow_width:center+inflow_width] += 100.0 # ๊ฐ•ํ•œ ์œ ๋Ÿ‰
# ์นจ์‹ ๋ฐ ํ‡ด์ 
erosion.simulate_transport(discharge, dt=dt)
# ์ธก๋ฐฉ ์นจ์‹ ํšจ๊ณผ (Banks collapse) - ๋‹จ์ˆœ ํ™•์‚ฐ์œผ๋กœ ๊ทผ์‚ฌ
# ๋ง์ƒํ•˜์ฒœ์€ ํ•˜์•ˆ์ด ๋ถˆ์•ˆ์ •ํ•จ
erosion.hillslope_diffusion(dt=dt * 0.1) # Diffusion Decreased (Sharper)
# [Fix] Water Depth
# Use flow accumulation to show braided channels
precip = np.ones((rows, cols)) * 0.01
inflow_width = max(3, n_channels * 2)
precip[0:2, center-inflow_width:center+inflow_width] += 50.0 # Source
discharge = hydro.route_flow_d8(precipitation=precip)
water_depth = np.log1p(discharge) * 0.3
water_depth[water_depth < 0.2] = 0 # Filter shallow flow
return {'elevation': grid.elevation, 'type': "๋ง์ƒ ํ•˜์ฒœ (Braided)", 'water_depth': water_depth}
@st.cache_data(ttl=3600)
def simulate_levee(time_years: int, params: dict, grid_size: int = 100):
"""
์ž์—ฐ์ œ๋ฐฉ ๋ฐ ๋ฐฐํ›„์Šต์ง€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (Process-Based)
- ํ™์ˆ˜ ๋ฒ”๋žŒ ์‹œ ์ˆ˜๋กœ ์ฃผ๋ณ€์— ์œ ์† ๊ฐ์†Œ -> ํ‡ด์  (์ž์—ฐ์ œ๋ฐฉ)
- ์ˆ˜๋กœ์—์„œ ๋ฉ€์–ด์งˆ์ˆ˜๋ก ๋ฏธ๋ฆฝ์งˆ ํ‡ด์  -> ๋ฐฐํ›„์Šต์ง€
"""
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
# 1. ์ดˆ๊ธฐ ์ง€ํ˜•: ํ‰ํƒ„ํ•œ ๋ฒ”๋žŒ์› + ์ค‘์•™ ์ˆ˜๋กœ
grid.bedrock[:] = 50.0
center_c = cols // 2
# Simple straight channel
channel_width_cells = 3
for c in range(center_c - channel_width_cells, center_c + channel_width_cells + 1):
grid.bedrock[:, c] -= 5.0 # Channel depth
grid.update_elevation()
# 2. ๋ฌผ๋ฆฌ ํ”„๋กœ์„ธ์Šค
hydro = HydroKernel(grid)
erosion = ErosionProcess(grid)
flood_freq = params.get('flood_freq', 0.5)
flood_magnitude = 10.0 + flood_freq * 20.0 # Flood height
steps = max(50, min(time_years // 100, 300))
dt = 1.0
# Sediment concentration in flood water
sediment_load = 0.5
# 3. ํ™์ˆ˜ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฃจํ”„
# ๋งค ์Šคํ…๋งˆ๋‹ค ํ™์ˆ˜๊ฐ€ ๋‚˜๋Š” ๊ฒƒ์€ ์•„๋‹ˆ์ง€๋งŒ, ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์ƒ์œผ๋กœ๋Š” ํ‡ด์  ๋ˆ„์ ์„ ๊ณ„์‚ฐ
# Simplified Model:
# Water Level rises -> Spreads sediment from channel -> Deposits close to bank
# Using 'diffusion' logic for suspended sediment
# Channel has high concentration (C=1). Floodplain has C=0 initially.
# Diffusion spreads C outwards.
# Deposition rate proportional to C.
# Or simplified physics:
# 1. Raise water level globally (Flood)
# 2. Add sediment source at channel
# 3. Diffuse sediment
# 4. Deposit
sediment_map = np.zeros((rows, cols)) # Instantaneous sediment in water
for i in range(steps):
# Flood Event
# Source at channel
sediment_map[:, center_c-channel_width_cells:center_c+channel_width_cells+1] = sediment_load
# Diffusion of sediment (Turbulent mixing)
# Using a gaussian or neighbor averaging loop is slow in Python.
# Use erosion.hillslope_diffusion trick on the sediment_map? No, that's for elevation.
# Simple Numpy diffusion:
# Lateral diffusion
for _ in range(5): # Diffusion steps per flood
sediment_map[:, 1:-1] = 0.25 * (sediment_map[:, :-2] + 2*sediment_map[:, 1:-1] + sediment_map[:, 2:])
# Deposition
# Deposit fraction of suspended sediment to ground
deposit_rate = 0.1 * dt
deposition = sediment_map * deposit_rate
# Don't deposit inside channel (kept clear by flow)
# Or deposit less? Natural levees form at bank, not bed.
# Bed is scoured.
# Mask channel
channel_mask = (grid.bedrock[:, center_c] < 46.0) # Check depth
# Better: use index
channel_indices = slice(center_c-channel_width_cells, center_c+channel_width_cells+1)
deposition[:, channel_indices] = 0
grid.sediment += deposition
# [Fix] Backswamp Water
# Low lying areas far from river might retain water if we simulated rain
# But here we just simulating formation.
# Raise channel bed slightly? No.
grid.update_elevation()
# Calculate Levee Height
levee_height = grid.sediment.max()
# [Fix] Water Depth
water_depth = np.zeros_like(grid.elevation)
water_depth[:, center_c-channel_width_cells:center_c+channel_width_cells+1] = 4.0 # Bankfull
# Backswamp water
# Areas where sediment is low (far away) -> Water table is close
# Visualize swamp
max_sed = grid.sediment.max()
swamp_mask = (grid.sediment < max_sed * 0.2) & (np.abs(np.arange(cols) - center_c) > 20)
water_depth[swamp_mask] = 0.5 # Shallow water
return {'elevation': grid.elevation, 'levee_height': levee_height, 'water_depth': water_depth}
@st.cache_data(ttl=3600)
def simulate_karst(theory: str, time_years: int, params: dict, grid_size: int = 100):
"""์นด๋ฅด์ŠคํŠธ ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฌผ๋ฆฌ ์—”์ง„ ์ ์šฉ - ํ™”ํ•™์  ์šฉ์‹)"""
# 1. ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™” (์„ํšŒ์•” ๋Œ€์ง€)
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
# ํ‰ํƒ„ํ•œ ๊ณ ์› (100m)
grid.bedrock[:] = 100.0
# ์•ฝ๊ฐ„์˜ ๋ถˆ๊ทœ์น™์„ฑ (์šฉ์‹ ์‹œ์ž‘์ )
np.random.seed(42)
grid.bedrock += np.random.rand(rows, cols) * 2.0
grid.update_elevation()
# 2. ์—”์ง„
hydro = HydroKernel(grid)
erosion = ErosionProcess(grid) # ๋ฌผ๋ฆฌ์  ์นจ์‹์€ ๋ฏธ๋ฏธํ•จ
co2 = params.get('co2', 0.5) # ์šฉ์‹ ํšจ์œจ
rainfall = params.get('rainfall', 0.5) # ๊ฐ•์ˆ˜๋Ÿ‰
# ํ™”ํ•™์  ์šฉ์‹ ๊ณ„์ˆ˜
dissolution_rate = 0.05 * co2
dt = 1.0
steps = 100
# ๋Œ๋ฆฌ๋„ค ์ดˆ๊ธฐ ์”จ์•— (Weak spots)
n_seeds = 5 + int(co2 * 5)
seeds = [(np.random.randint(10, rows-10), np.random.randint(10, cols-10)) for _ in range(n_seeds)]
for cx, cy in seeds:
# ์ดˆ๊ธฐ ํ•จ๋ชฐ
grid.bedrock[cx, cy] -= 5.0
grid.update_elevation()
for i in range(steps):
# [Hybrid Approach]
# ๊ต๊ณผ์„œ์ ์ธ ๋Œ๋ฆฌ๋„ค(Doline) ํ˜•ํƒœ ๊ฐ•์ œ (Round Depression)
# 1. Physics (Dissolution) - keep it mostly for creating the *seeds*
# But force the shape to be round
# Aggressive deepening at seeds
for cx, cy in seeds:
Y, X = np.ogrid[:grid_size, :grid_size]
dist = np.sqrt((X - cx)**2 + (Y - cy)**2)
# Bowl shape
# depth increases with time
current_depth = (i / steps) * 30.0 * co2
radius = 5.0 + (i/steps)*5.0
mask = dist < radius
depression = current_depth * (1.0 - (dist[mask]/radius)**2)
# Apply max depth (don't double dip if overlapping)
# We want to subtract.
# grid.bedrock[mask] = min(grid.bedrock[mask], 100.0 - depression)
# Simpler: subtract increment
# Re-implement: Just carve analytical bowls at the END?
# No, iterative is better for animation.
pass
# Finalize Shape (Force Round Bowls)
# [Fix] Scale evolution by time
evolution = min(1.0, time_years / 50000.0)
for cx, cy in seeds:
Y, X = np.ogrid[:grid_size, :grid_size]
dist = np.sqrt((X - cx)**2 + (Y - cy)**2)
# Grow radius and depth
radius = 3.0 + 7.0 * evolution # 3m -> 10m
mask = dist < radius
# Ideal Bowl
depth = 20.0 * co2 * evolution
profile = 100.0 - depth * (1.0 - (dist/radius)**2)
grid.bedrock = np.where(mask, np.minimum(grid.bedrock, profile), grid.bedrock)
# U-Valley or Karst Valley?
# Just Dolines for now.
max_depth = 100.0 - grid.bedrock.min()
return {'elevation': grid.bedrock, 'depth': max_depth, 'n_dolines': n_seeds}
@st.cache_data(ttl=3600)
def simulate_tower_karst(time_years: int, params: dict, grid_size: int = 100):
"""ํƒ‘ ์นด๋ฅด์ŠคํŠธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - ์ฐจ๋ณ„ ์šฉ์‹"""
x = np.linspace(0, 1000, grid_size)
y = np.linspace(0, 1000, grid_size)
X, Y = np.meshgrid(x, y)
# [Hybrid Approach]
# ๊ต๊ณผ์„œ์ ์ธ ํƒ‘ ์นด๋ฅด์ŠคํŠธ (Steep Towers) ๊ฐ•์ œ
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
# 1. Base Plain
grid.bedrock[:] = 20.0
# 2. Towers (Random distribution but sharp)
np.random.seed(99)
n_towers = 15
centers = [(np.random.randint(10, 90), np.random.randint(10, 90)) for _ in range(n_towers)]
Y, X = np.ogrid[:grid_size, :grid_size]
towers_elev = np.zeros_like(grid.bedrock)
for cx, cy in centers:
dist = np.sqrt((X - cx)**2 + (Y - cy)**2)
# Tower Shape: Steep sides, rounded top (Sugarloaf)
radius = 8.0
# [Fix] Towers become more prominent (or surrounding erodes) over time
# Assume surrounding erodes, making towers relatively higher?
# Or assume towers grow? Simulation subtracts from 20m plane?
# Ah, sim adds `towers_elev` to `grid.bedrock`.
# Let's scale height.
evolution = min(1.0, time_years / 100000.0)
target_height = 50.0 + np.random.rand() * 50.0
height = target_height * evolution
# Profile:
# if dist < radius: h * exp(...)
# make it steeper than gaussian
shape = height * (1.0 / (1.0 + np.exp((dist - radius)*1.0)))
towers_elev = np.maximum(towers_elev, shape)
grid.bedrock += towers_elev
return {'elevation': grid.bedrock, 'type': "ํƒ‘ ์นด๋ฅด์ŠคํŠธ (Tower)"}
@st.cache_data(ttl=3600)
def simulate_cave(time_years: int, params: dict, grid_size: int = 100):
"""์„ํšŒ ๋™๊ตด ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - ์„์ˆœ/์ข…์œ ์„ ์„ฑ์žฅ (๋ฐ”๋‹ฅ๋ฉด ๊ธฐ์ค€)"""
x = np.linspace(0, 100, grid_size)
y = np.linspace(0, 100, grid_size)
X, Y = np.meshgrid(x, y)
elevation = np.zeros((grid_size, grid_size))
# ๋™๊ตด ๋ฐ”๋‹ฅ (ํ‰ํƒ„)
# ์„์ˆœ (Stalagmites) ์„ฑ์žฅ
# ๋žœ๋ค ์œ„์น˜์— ์”จ์•—
np.random.seed(42)
n_stalagmites = 10
centers = [(np.random.randint(20, 80), np.random.randint(20, 80)) for _ in range(n_stalagmites)]
growth_rate = params.get('rate', 0.5)
steps = max(1, time_years // 100)
total_growth = steps * growth_rate * 0.05
for cx, cy in centers:
# ๊ฐ€์šฐ์‹œ์•ˆ ํ˜•์ƒ
dist = np.sqrt((X - cx)**2 + (Y - cy)**2)
# ์„ฑ์žฅ: ๋†’์ด์™€ ๋„ˆ๋น„๊ฐ€ ๊ฐ™์ด ์ปค์ง
h = total_growth * (0.8 + np.random.rand()*0.4)
w = h * 0.3 # ๋พฐ์กฑํ•˜๊ฒŒ
shape = h * np.exp(-(dist**2)/(w**2 + 1))
elevation = np.maximum(elevation, shape)
return {'elevation': elevation, 'type': "์„ํšŒ๋™๊ตด (Cave)"}
@st.cache_data(ttl=3600)
def simulate_volcanic(theory: str, time_years: int, params: dict, grid_size: int = 100):
"""ํ™”์‚ฐ ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฌผ๋ฆฌ ์—”์ง„ ์ ์šฉ - ์šฉ์•” ์œ ๋™)"""
# 1. ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™”
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
center = cols // 2
# ๊ธฐ๋ฐ˜ ์ง€ํ˜• (ํ‰์ง€)
grid.bedrock[:] = 50.0
# 2. ์—”์ง„ (์šฉ์•” ํ๋ฆ„ -> HydroKernel ์‘์šฉ)
hydro = HydroKernel(grid)
# ์šฉ์•”์€ ๋ฌผ๋ณด๋‹ค ์ ์„ฑ์ด ๋งค์šฐ ๋†’์Œ -> ํ™•์‚ฐ์ด ์ž˜ ์•ˆ๋˜๊ณ  ์Œ“์ž„
# ์—ฌ๊ธฐ์„œ๋Š” 'Sediment'๋ฅผ ์šฉ์•”์œผ๋กœ ๊ฐ„์ฃผํ•˜์—ฌ ์Œ“์ด๊ฒŒ ํ•จ
eruption_rate = params.get('eruption_rate', 0.5)
lava_viscosity = 0.5 # ์ ์„ฑ
# [Hybrid Approach]
# ๊ต๊ณผ์„œ์ ์ธ ํ™”์‚ฐ(Cone/Shield) ํ˜•ํƒœ ๊ฐ•์ œ
# 1. Ideal Volcano Shape
# Cone (Strato) or Dome (Shield)
Y, X = np.ogrid[:grid_size, :grid_size]
# Center
cx, cy = grid_size//2, grid_size//2
dist = np.sqrt((X - cx)**2 + (Y - cy)**2)
volcano_h = 0.0
if theory == "shield":
# Shield: Wide, gentle slope (Gaussian)
volcano_h = 100.0 * np.exp(-(dist**2)/(40**2))
elif theory == "strato":
# Strato: Steep, concave (Exponential)
volcano_h = 150.0 * np.exp(-dist/15.0)
elif theory == "caldera":
# Caldera: Strato then cut top
base_h = 150.0 * np.exp(-dist/15.0)
# Cut top (Crater)
crater_mask = dist < 20
base_h[crater_mask] = 80.0 # Floor
# Rim
rim_mask = (dist >= 20) & (dist < 25)
# Smooth transition is tricky, just hard cut for "Textbook" look
volcano_h = base_h
# Apply to Sediment (Lava)
# [Fix] Scale height by time
growth = min(1.0, time_years / 50000.0)
grid.sediment += volcano_h * growth
# 2. Add Flow Textures (Physics)
hydro = HydroKernel(grid)
steps = 50
for i in range(steps):
# Add slight roughness/flow lines
erosion = ErosionProcess(grid)
erosion.hillslope_diffusion(dt=1.0)
# ์ตœ์ข… ์ง€ํ˜• = ๊ธฐ๋ฐ˜์•” + ์šฉ์•”
grid.update_elevation()
volcano_type = theory.capitalize()
height = grid.elevation.max() - 50.0
return {'elevation': grid.elevation, 'height': height, 'type': volcano_type}
@st.cache_data(ttl=3600)
def simulate_lava_plateau(time_years: int, params: dict, grid_size: int = 100):
"""์šฉ์•” ๋Œ€์ง€ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - ์—ดํ•˜ ๋ถ„์ถœ"""
x = np.linspace(-500, 500, grid_size)
y = np.linspace(-500, 500, grid_size)
X, Y = np.meshgrid(x, y)
# ๊ธฐ์กด ์ง€ํ˜• (์šธํ‰๋ถˆํ‰ํ•œ ์‚ฐ์ง€)
elevation = 50 * np.sin(X/100) * np.cos(Y/100) + 20 * np.random.rand(grid_size, grid_size)
# ์—ดํ•˜ ๋ถ„์ถœ (Fissure Eruption)
# ์ค‘์•™์„ ๊ฐ€๋กœ์ง€๋ฅด๋Š” ํ‹ˆ
fissure_width = 10
fissure_mask = np.abs(X) < fissure_width
eruption_rate = params.get('eruption_rate', 0.5)
steps = max(1, time_years // 100)
# ์šฉ์•”๋ฅ˜ ์ฑ„์šฐ๊ธฐ (Flood Fill logic simplified)
# ๋‚ฎ์€ ๊ณณ๋ถ€ํ„ฐ ์ฑ„์›Œ์ ธ์„œ ํ‰ํƒ„ํ•ด์ง
total_volume = steps * eruption_rate * 1000
current_level = elevation.min()
# ๊ฐ„๋‹จํ•œ ์ˆ˜์œ„ ์ƒ์Šน ๋ชจ๋ธ (ํ‰ํƒ„ํ™”)
# ์šฉ์•”์€ ์œ ๋™์„ฑ์ด ์ปค์„œ ์ˆ˜ํ‰์„ ์œ ์ง€ํ•˜๋ ค ํ•จ
# [Fix] Scale level by time
growth = min(1.0, time_years / 50000.0)
target_level = current_level + (total_volume / (grid_size**2) * 2) * growth # ๋Œ€๋žต์  ๋†’์ด ์ฆ๊ฐ€
# ๊ธฐ์กด ์ง€ํ˜•๋ณด๋‹ค ๋‚ฎ์€ ๊ณณ์€ ์šฉ์•”์œผ๋กœ ์ฑ„์›€ (ํ‰ํƒ„๋ฉด ํ˜•์„ฑ)
# But only up to target_level
lava_cover = np.maximum(elevation, target_level)
# Actually, we should fill ONLY if elevation < target_level
# And preserve mountains above target_level
# logic: new_h = max(old_h, target_level) is correct for filling valleys
# ๊ฐ€์žฅ์ž๋ฆฌ๋Š” ์•ฝ๊ฐ„ ํ๋ฆ„ (๊ฒฝ์‚ฌ)
dist_from_center = np.abs(X)
lava_cover = np.where(dist_from_center < 400, lava_cover, np.minimum(lava_cover, elevation + (lava_cover-elevation)*np.exp(-(dist_from_center-400)/50)))
return {'elevation': lava_cover, 'type': "์šฉ์•” ๋Œ€์ง€ (Lava Plateau)"}
@st.cache_data(ttl=3600)
def simulate_columnar_jointing(time_years: int, params: dict, grid_size: int = 100):
"""์ฃผ์ƒ์ ˆ๋ฆฌ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - ์œก๊ฐ ๊ธฐ๋‘ฅ ํŒจํ„ด"""
x = np.linspace(-20, 20, grid_size)
y = np.linspace(-20, 20, grid_size)
X, Y = np.meshgrid(x, y)
# ๊ธฐ๋ณธ ์šฉ์•” ๋Œ€์ง€ (ํ‰ํƒ„)
elevation = np.ones((grid_size, grid_size)) * 100
# ์œก๊ฐํ˜• ํŒจํ„ด ์ƒ์„ฑ (๊ฐ„๋‹จํ•œ ์ˆ˜ํ•™์  ๊ทผ์‚ฌ)
# Cosine ๊ฐ„์„ญ์œผ๋กœ ๋ฒŒ์ง‘ ๋ชจ์–‘ ์œ ์‚ฌ ํŒจํ„ด ์ƒ์„ฑ
scale = 2.0
hex_pattern = np.cos(X*scale) + np.cos((X/2 + Y*np.sqrt(3)/2)*scale) + np.cos((X/2 - Y*np.sqrt(3)/2)*scale)
# ๊ธฐ๋‘ฅ์˜ ๋†’์ด ์ฐจ์ด (ํ’ํ™”)
erosion_rate = params.get('erosion_rate', 0.5)
steps = max(1, time_years // 100)
# [Fix] Scale weathering by time
weathering = (steps * erosion_rate * 0.05) * (time_years / 10000.0)
# ์ ˆ๋ฆฌ(ํ‹ˆ) ๋ถ€๋ถ„์€ ๋‚ฎ๊ฒŒ, ๊ธฐ๋‘ฅ ์ค‘์‹ฌ์€ ๋†’๊ฒŒ
# hex_pattern > 0 ์ธ ๋ถ€๋ถ„์ด ๊ธฐ๋‘ฅ
elevation += hex_pattern * 5 # ๊ธฐ๋‘ฅ ๊ตด๊ณก
# ์นจ์‹ ์ž‘์šฉ (ํ‹ˆ์ด ๋” ๋งŽ์ด ๊นŽ์ž„)
cracks = hex_pattern < -1.0 # ์ ˆ๋ฆฌ ํ‹ˆ
# [Fix] Deepen cracks over time
crack_depth = 20 + weathering * 10
elevation[cracks] -= crack_depth
# ์ „์ฒด์ ์ธ ๋‹จ๋ฉด (ํ•ด์•ˆ ์ ˆ๋ฒฝ ๋А๋‚Œ)
# Y < 0 ์ธ ๋ถ€๋ถ„์€ ๋ฐ”๋‹ค์ชฝ์œผ๋กœ ๊นŽ์ž„
cliff_mask = Y < -10
elevation[cliff_mask] -= 50
return {'elevation': elevation, 'type': "์ฃผ์ƒ์ ˆ๋ฆฌ (Columnar Jointing)"}
@st.cache_data(ttl=3600)
def simulate_glacial(theory: str, time_years: int, params: dict, grid_size: int = 100):
"""๋น™ํ•˜ ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฌผ๋ฆฌ ์—”์ง„ - ๋น™ํ•˜ ์นจ์‹ Q^0.5)"""
# [Hybrid Approach]
# ๊ต๊ณผ์„œ์ ์ธ U์ž๊ณก ํ˜•ํƒœ๋ฅผ ๊ฐ•์ œ(Template)ํ•˜๊ณ , ๋ฌผ๋ฆฌ ์—”์ง„์œผ๋กœ ์งˆ๊ฐ๋งŒ ์ž…ํž˜
rows, cols = grid_size, grid_size
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
ice_thickness = params.get('ice_thickness', 1.0)
# 1. Ideal U-Shape Template
center = cols // 2
u_width = 30 # Half width
# [Fix] Time-dependent depth and shape
evolution = min(1.0, time_years / 100000.0)
# ์‹œ๊ฐ„์— ๋”ฐ๋ฅธ ๊นŠ์ด ๋ฐ ๋„ˆ๋น„ ์ง„ํ™”
# Ice thickness determines depth
target_depth = 200 * ice_thickness * (0.2 + 0.8 * evolution)
shape_exp = 1.5 + 2.5 * evolution # Morph from V(1.5) to U(4.0)
# Create U-profile
dist_from_center = np.abs(np.arange(cols) - center)
# U-Shape function: Flat bottom, steep walls
# Profile ~ (x/w)^4
normalized_dist = np.minimum(dist_from_center / u_width, 1.5)
u_profile = -target_depth * (1.0 - np.power(normalized_dist, shape_exp))
u_profile = np.maximum(u_profile, -target_depth) # Cap depth
# Apply to grid rows
# V-valley was initial. We morph V to U.
for r in range(rows):
# Base slope
base_h = 300 - (r/rows)*200
grid.bedrock[r, :] = base_h + u_profile
# 2. Add Physics Detail (Roughness)
steps = 50
hydro = HydroKernel(grid)
grid.update_elevation()
for i in range(steps):
# Slight erosion to add texture
precip = np.ones((rows, cols)) * 0.05
discharge = hydro.route_flow_d8(precipitation=precip)
# Glacial Polish/Plucking noise
erosion_amount = discharge * 0.001
grid.bedrock -= erosion_amount
# Fjord Handling
valley_type = "๋น™์‹๊ณก (U์ž๊ณก)"
if theory == "fjord":
grid.bedrock -= 120 # Submerge
grid.bedrock = np.maximum(grid.bedrock, -50)
valley_type = "ํ”ผ์˜ค๋ฅด (Fjord)"
grid.update_elevation()
depth = grid.bedrock.max() - grid.bedrock.min()
return {'elevation': grid.bedrock, 'width': 300, 'depth': depth, 'type': valley_type}
@st.cache_data(ttl=3600)
def simulate_cirque(time_years: int, params: dict, grid_size: int = 100):
"""๊ถŒ๊ณก ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - ํšŒ์ „ ์Šฌ๋ผ์ด๋”ฉ"""
x = np.linspace(0, 1000, grid_size)
y = np.linspace(0, 1000, grid_size)
X, Y = np.meshgrid(x, y)
# ์ดˆ๊ธฐ ์‚ฐ ์‚ฌ๋ฉด (๊ฒฝ์‚ฌ)
elevation = Y * 0.5 + 100
# ๊ถŒ๊ณก ํ˜•์„ฑ ์œ„์น˜ (์ค‘์•™ ์ƒ๋ถ€)
cx, cy = 500, 700
r = 250
dt = 100
steps = max(1, time_years // dt)
erosion_rate = params.get('erosion_rate', 0.5)
# ์‹œ๊ฐ„ ์ง„ํ–‰
total_erosion = min(1.0, steps * erosion_rate * 0.001)
# [Hybrid Approach] Check
# ๊ต๊ณผ์„œ์ ์ธ ๊ถŒ๊ณก(Bowl) ํ˜•ํƒœ ๊ฐ•์ œ
# Ideal Bowl Shape
# cx, cy center
dx = X - cx
dy = Y - cy
dist = np.sqrt(dx**2 + dy**2)
# Bowl depth profile
# Deepest at 0.5r, Rim at 1.0r
bowl_mask = dist < r
# Armchair shape: Steep backwall, Deep basin, Shallow lip
# Backwall (Y > cy)
normalized_y = (Y - cy) / r
backwall_effect = np.clip(normalized_y, -1, 1)
# Excavation amount
excavation = np.zeros_like(elevation)
# Basic Bowl
excavation[bowl_mask] = 100 * (1 - (dist[bowl_mask]/r)**2)
# Deepen the back (Cirque characteristic)
excavation[bowl_mask] *= (1.0 + backwall_effect[bowl_mask] * 0.5)
# Parameter scaling
total_effect = min(1.0, steps * erosion_rate * 0.01)
elevation -= excavation * total_effect
# Make Rim sharp (Arete precursor)
# Add roughness
noise = np.random.rand(grid_size, grid_size) * 5.0
elevation += noise
return {'elevation': elevation, 'type': "๊ถŒ๊ณก (Cirque)"}
@st.cache_data(ttl=3600)
def simulate_moraine(time_years: int, params: dict, grid_size: int = 100):
"""๋ชจ๋ ˆ์ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฌผ๋ฆฌ ์—”์ง„ - ๋น™ํ•˜ ํ‡ด์ )"""
# 1. ๊ทธ๋ฆฌ๋“œ (U์ž๊ณก ๊ธฐ๋ฐ˜)
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
# U์ž๊ณก ํ˜•ํƒœ ์ƒ์„ฑ
center = cols // 2
for r in range(rows):
grid.bedrock[r, :] = 200 - (r/rows) * 100
for c in range(cols):
dist_norm = abs(c - center) / (cols/2)
# U-shape profile: flat bottom, steep sides
u_profile = (dist_norm ** 4) * 150
grid.bedrock[:, c] += u_profile
# 2. ํ‡ด์  ์‹œ๋ฎฌ๋ ˆ์ด์…˜
debris_supply = params.get('debris_supply', 0.5)
# ๋น™ํ•˜ ๋(Terminus) ์œ„์น˜ ๋ณ€ํ™”
# 100๋…„ -> ๋(row=cols), 10000๋…„ -> ํ›„ํ‡ด(row=0)
# ์—ฌ๋Ÿฌ ๋‹จ๊ณ„์— ๊ฑธ์ณ ํ‡ด์ 
# [Hybrid Approach]
# ๊ต๊ณผ์„œ์ ์ธ ๋ชจ๋ ˆ์ธ(Ridge) ํ˜•ํƒœ ๊ฐ•์ œ
# [Fix] Time-dependent retreat
# 20,000 years for full retreat
retreat_progress = min(1.0, time_years / 20000.0)
# We shouldn't loop 50 steps to build the final shape if time is fixed?
# Actually, we want to show the accumulated sediment up to 'retreat_progress'.
# So we iterate up to current progress.
total_steps = 50
current_steps = int(total_steps * retreat_progress)
for i in range(current_steps + 1):
# ๋น™ํ•˜ ๋ ์œ„์น˜ (Dynamic Retreat)
# 0 -> 1 (Progress)
p = i / total_steps
terminus_row = int(rows * (0.8 - p * 0.6))
# 1. Terminal Moraine (Arc)
# ํ‡ด์ ๋ฌผ ์ง‘์ค‘ (Ridge)
# Gaussian ridge at terminus_row
# Arc shape: slightly curved back upstream at edges
# Deposit mainly at terminus
current_flux = debris_supply * 5.0
# Create a ridge mask
# 2D Gaussian Ridge?
# Just simple row addition with noise
# Arc curvature
curvature = 10
for c in range(cols):
# Row shift for arc
dist_c = abs(c - center) / (cols/2)
arc_shift = int(dist_c * dist_c * curvature)
target_r = terminus_row - arc_shift
if 0 <= target_r < rows:
# Add sediment pile
# "Recessional Moraines" - leave small piles as it retreats
# "Terminal Moraine" - The biggest one at max extent (start)
amount = current_flux
if i == 0: amount *= 3.0 # Main terminal moraine is huge
# Deposit
if grid.sediment[target_r, c] < 50: # Limit height
grid.sediment[target_r, c] += amount
# 2. Lateral Moraine (Side ridges)
# Always deposit at edges of glacier (which is u_profile width)
# Glacier width ~ where u_profile starts rising steep
glacier_width_half = cols // 4
# Left Lateral
l_c = center - glacier_width_half
grid.sediment[terminus_row:, l_c-2:l_c+3] += current_flux * 0.5
# Right Lateral
r_c = center + glacier_width_half
grid.sediment[terminus_row:, r_c-2:r_c+3] += current_flux * 0.5
# Smoothing
erosion = ErosionProcess(grid)
erosion.hillslope_diffusion(dt=5.0)
return {'elevation': grid.elevation, 'type': "๋ชจ๋ ˆ์ธ (Moraine)"}
@st.cache_data(ttl=3600)
def simulate_arid(theory: str, time_years: int, params: dict, grid_size: int = 100):
"""๊ฑด์กฐ ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (๋ฌผ๋ฆฌ ์—”์ง„ - ๋ฐ”๋žŒ ์ด๋™ ๋ฐ ์นจ์‹)"""
# 1. ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™”
cell_size = 1000.0 / grid_size
grid = WorldGrid(width=grid_size, height=grid_size, cell_size=cell_size)
rows, cols = grid_size, grid_size
steps = 100
wind_speed = params.get('wind_speed', 0.5)
# 2. ์ด๋ก ๋ณ„ ์—”์ง„ ์ ์šฉ
if theory == "barchan":
# [Hybrid Approach]
# ๊ต๊ณผ์„œ์ ์ธ ์ดˆ์Šน๋‹ฌ(Crescent) ๋ชจ์–‘ ๊ฐ•์ œ
# Analytical Barchan Shape
# Center of dune
cx, cy = grid_size // 2, grid_size // 3
# Coordinate relative to center
Y, X = np.ogrid[:grid_size, :grid_size]
dx = X - cx
dy = Y - cy
# Dune Size
W = 15.0 # Width param
L = 15.0 # Length param
# Crescent Formula (simplified)
# Body: Gaussian
body = 40.0 * np.exp(-(dx**2 / (W**2) + dy**2 / (L**2)))
# Horns: Subtract parabolic shape from behind
# Wind from X (left to right) -> Horns point right
# Cutout from the back
cutout = 30.0 * np.exp(-((dx + 10)**2 / (W*1.5)**2 + dy**2 / (L*0.8)**2))
dune_shape = np.maximum(0, body - cutout)
# ๋ฟ”(Horn)์„ ๋” ๊ธธ๊ฒŒ ์•ž์œผ๋กœ ๋‹น๊น€
# Bending
horns = 10.0 * np.exp(-(dy**2 / 100.0)) * np.exp(-((dx-10)**2 / 200.0))
# Mask horns to be mainly on sides
horns_mask = (np.abs(dy) > 10) & (dx > 0)
dune_shape[horns_mask] += horns[horns_mask] * 2.0
# Apply to Sediment
grid.sediment[:] = dune_shape
# Physics Drift (Winds)
# 1. Advection (Move Downwind)
# [Fix] Move based on time
shift_amount = int(wind_speed * time_years * 0.05) % cols
if shift_amount > 0:
grid.sediment = np.roll(grid.sediment, shift_amount, axis=1) # Move Right
grid.sediment[:, :shift_amount] = 0
# 2. Diffusion (Smooth slopes)
erosion = ErosionProcess(grid)
erosion.hillslope_diffusion(dt=5.0)
landform_type = "๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ (Barchan)"
elif theory == "mesa":
# [Hybrid Approach]
# ๊ต๊ณผ์„œ์ ์ธ ๋ฉ”์‚ฌ(Table) ํ˜•ํƒœ ๊ฐ•์ œ
# 1. Base Plateau
grid.bedrock[:] = 20.0
# 2. Hard Caprock (Circle or Rectangle)
# Center high
cx, cy = grid_size // 2, grid_size // 2
Y, X = np.ogrid[:grid_size, :grid_size]
dist = np.sqrt((X - cx)**2 + (Y - cy)**2)
# Mesa Radius
# [Fix] Mesa shrinks over time (Cliff Backwearing)
# Start wide (Plateau), shrink to Mesa
initial_r = 45.0
shrinkage = (time_years / 100000.0) * 20.0 # Shrink 20m over 100ky
mesa_r = max(10.0, initial_r - shrinkage)
# Steep Cliff (Sigmoid or Step)
# Height 100m
height_profile = 80.0 * (1.0 / (1.0 + np.exp((dist - mesa_r) * 1.0)))
grid.bedrock += height_profile
# 3. Physics Erosion (Talus formation)
# ์นจ์‹์‹œ์ผœ์„œ ์ ˆ๋ฒฝ ๋ฐ‘์— ์• ์ถ”(Talus) ํ˜•์„ฑ
steps = 50
erosion = ErosionProcess(grid, K=0.005) # Weak erosion
hydro = HydroKernel(grid)
for i in range(steps):
precip = np.ones((rows, cols)) * 0.05
discharge = hydro.route_flow_d8(precipitation=precip)
# Cliff retreat (very slow)
# Talus accumulation (High diffusion on slopes)
erosion.hillslope_diffusion(dt=2.0)
landform_type = "๋ฉ”์‚ฌ (Mesa)"
elif theory == "pediment":
# ํŽ˜๋””๋จผํŠธ: ์‚ฐ์ง€ ์•ž์˜ ์™„๊ฒฝ์‚ฌ ์นจ์‹๋ฉด
# ์‚ฐ(High) -> ํŽ˜๋””๋จผํŠธ(Slope) -> ํ”Œ๋ผ์•ผ(Flat)
# Mountain Back
grid.bedrock[:30, :] = 150.0
# Pediment Slope (Linear)
for r in range(30, 80):
grid.bedrock[r, :] = 150.0 - (r-30) * 2.5 # 150 -> 25
# Playa (Flat)
grid.bedrock[80:, :] = 25.0
# Noise
grid.bedrock += np.random.rand(rows, cols) * 2.0
landform_type = "ํŽ˜๋””๋จผํŠธ (Pediment)"
else:
landform_type = "๊ฑด์กฐ ์ง€ํ˜•"
grid.update_elevation()
return {'elevation': grid.elevation, 'type': landform_type}
@st.cache_data(ttl=3600)
def simulate_plain(theory: str, time_years: int, params: dict, grid_size: int = 100):
"""ํ‰์•ผ ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ด์…˜ - ๊ต๊ณผ์„œ์  ๋ฒ”๋žŒ์›, ์ž์—ฐ์ œ๋ฐฉ"""
x = np.linspace(0, 1000, grid_size)
y = np.linspace(0, 1000, grid_size)
X, Y = np.meshgrid(x, y)
# ๊ธฐ๋ณธ ํ‰ํƒ„ ์ง€ํ˜• (์•ฝ๊ฐ„ ์ƒ๋ฅ˜๊ฐ€ ๋†’์Œ)
base_height = 20
elevation = np.ones((grid_size, grid_size)) * base_height
elevation += 5 * (1 - Y / 1000)
flood_freq = params.get('flood_freq', 0.5)
# ํ•˜์ฒœ ์ค‘์‹ฌ์„  (์•ฝ๊ฐ„ ์‚ฌํ–‰)
river_x = 500 + 30 * np.sin(np.linspace(0, 3*np.pi, grid_size))
if theory == "floodplain" or theory == "levee":
# ๊ต๊ณผ์„œ์  ๋ฒ”๋žŒ์›: ์ž์—ฐ์ œ๋ฐฉ > ๋ฐฐํ›„์Šต์ง€ ๊ตฌ์กฐ
# [Fix] Scale Levee growth by time and flood_freq
# e.g. 1000 years -> small levee, 100,000 years -> huge levee
time_factor = min(1.0, time_years / 50000.0)
levee_growth = (base_height + 4 + flood_freq * 2 - base_height) * (0.2 + 0.8 * time_factor)
backswamp_growth = 0 # stays low usually, or fills up slowly?
for j in range(grid_size):
rx = int(river_x[j] * grid_size / 1000)
if 0 < rx < grid_size:
# ํ•˜์ฒœ (๊ฐ€์žฅ ๋‚ฎ์Œ)
river_width = 3
for i in range(max(0, rx-river_width), min(grid_size, rx+river_width)):
elevation[j, i] = base_height - 5
# ์ž์—ฐ์ œ๋ฐฉ (ํ•˜์ฒœ ์–‘์ชฝ, ๋†’์Œ)
levee_width = 12
# [Fix] Dynamic height
levee_height = base_height + levee_growth
for i in range(max(0, rx-levee_width), rx-river_width):
dist = abs(i - rx)
elevation[j, i] = levee_height - (dist - river_width) * 0.2
for i in range(rx+river_width, min(grid_size, rx+levee_width)):
dist = abs(i - rx)
elevation[j, i] = levee_height - (dist - river_width) * 0.2
# ๋ฐฐํ›„์Šต์ง€ (์ž์—ฐ์ œ๋ฐฉ ๋ฐ”๊นฅ์ชฝ, ๋‚ฎ์Œ)
backswamp_height = base_height - 2
for i in range(0, max(0, rx-levee_width)):
elevation[j, i] = backswamp_height
for i in range(min(grid_size, rx+levee_width), grid_size):
elevation[j, i] = backswamp_height
plain_type = "๋ฒ”๋žŒ์›"
elif theory == "alluvial":
# ์ถฉ์ ํ‰์•ผ (์ „์ฒด์ ์œผ๋กœ ํ‡ด์ )
for j in range(grid_size):
rx = int(river_x[j] * grid_size / 1000)
dist_from_river = np.abs(np.arange(grid_size) - rx)
deposition = flood_freq * 3 * np.exp(-dist_from_river / 30)
elevation[j, :] += deposition
elevation[j, max(0,rx-2):min(grid_size,rx+2)] = base_height - 3
plain_type = "์ถฉ์ ํ‰์•ผ"
else:
plain_type = "ํ‰์•ผ"
# [Fix] Water Depth
water_depth = np.zeros_like(elevation)
for j in range(grid_size):
rx = int(river_x[j] * grid_size / 1000)
if 0 < rx < grid_size:
river_width = 3
water_depth[j, max(0, rx-river_width):min(grid_size, rx+river_width+1)] = 3.0
return {'elevation': elevation, 'type': plain_type, 'water_depth': water_depth}
# ============ ์‚ฌ์‹ค์  ๋ Œ๋”๋ง ============
def create_terrain_colormap():
"""์ž์—ฐ์Šค๋Ÿฌ์šด ์ง€ํ˜• ์ƒ‰์ƒ๋งต"""
# ๊ณ ๋„๋ณ„ ์ƒ‰์ƒ: ๋ฌผ(ํŒŒ๋ž‘) โ†’ ํ•ด์•ˆ(ํ™ฉํ† ) โ†’ ์ €์ง€๋Œ€(๋…น์ƒ‰) โ†’ ์‚ฐ์ง€(๊ฐˆ์ƒ‰) โ†’ ๊ณ ์‚ฐ(ํฐ์ƒ‰)
cdict = {
'red': [(0.0, 0.1, 0.1), (0.25, 0.9, 0.9), (0.4, 0.4, 0.4),
(0.6, 0.6, 0.6), (0.8, 0.5, 0.5), (1.0, 1.0, 1.0)],
'green': [(0.0, 0.3, 0.3), (0.25, 0.85, 0.85), (0.4, 0.7, 0.7),
(0.6, 0.5, 0.5), (0.8, 0.35, 0.35), (1.0, 1.0, 1.0)],
'blue': [(0.0, 0.6, 0.6), (0.25, 0.6, 0.6), (0.4, 0.3, 0.3),
(0.6, 0.3, 0.3), (0.8, 0.2, 0.2), (1.0, 1.0, 1.0)]
}
return colors.LinearSegmentedColormap('terrain_natural', cdict)
def render_terrain_3d(elevation, title, add_water=True, water_level=0, view_elev=35, view_azim=225):
"""3D Perspective ๋ Œ๋”๋ง - ๋‹จ์ผ ์ƒ‰์ƒ(copper)"""
fig = plt.figure(figsize=(12, 9), facecolor='#1a1a2e')
ax = fig.add_subplot(111, projection='3d', facecolor='#1a1a2e')
h, w = elevation.shape
x = np.arange(w)
y = np.arange(h)
X, Y = np.meshgrid(x, y)
# ๋‹จ์ผ ์ƒ‰์ƒ (copper - ๊ฐˆ์ƒ‰ ๋ช…๋„ ๋ณ€ํ™”)
elev_norm = (elevation - elevation.min()) / (elevation.max() - elevation.min() + 0.01)
surf = ax.plot_surface(X, Y, elevation,
facecolors=cm.copper(elev_norm),
linewidth=0, antialiased=True,
shade=True, lightsource=plt.matplotlib.colors.LightSource(315, 45))
# ๋ฌผ ํ‘œ๋ฉด (์–ด๋‘์šด ์ƒ‰์ƒ)
if add_water:
water_mask = elevation < water_level
if np.any(water_mask):
ax.plot_surface(X, Y, np.where(water_mask, water_level, np.nan),
color='#2C3E50', alpha=0.8, linewidth=0)
ax.view_init(elev=view_elev, azim=view_azim)
# ์ถ• ์Šคํƒ€์ผ
ax.set_xlabel('X (m)', fontsize=10, color='white')
ax.set_ylabel('Y (m)', fontsize=10, color='white')
ax.set_zlabel('๊ณ ๋„ (m)', fontsize=10, color='white')
ax.set_title(title, fontsize=14, fontweight='bold', pad=20, color='white')
ax.tick_params(colors='white')
# ์ปฌ๋Ÿฌ๋ฐ” (copper)
mappable = cm.ScalarMappable(cmap='copper',
norm=plt.Normalize(elevation.min(), elevation.max()))
mappable.set_array([])
cbar = fig.colorbar(mappable, ax=ax, shrink=0.5, aspect=15, pad=0.1)
cbar.set_label('๊ณ ๋„ (m)', fontsize=10, color='white')
cbar.ax.yaxis.set_tick_params(color='white')
plt.setp(plt.getp(cbar.ax.axes, 'yticklabels'), color='white')
if add_water:
water_patch = mpatches.Patch(color='#2C3E50', alpha=0.8, label='์ˆ˜์—ญ')
ax.legend(handles=[water_patch], loc='upper left', fontsize=9,
facecolor='#1a1a2e', labelcolor='white')
# ํ˜„์žฌ ์ง€ํ˜•๋ฉด
plt.tight_layout()
return fig
def render_terrain_plotly(elevation, title, add_water=True, water_level=0, texture_path=None, force_camera=True, water_depth_grid=None, sediment_grid=None, landform_type=None):
"""Plotly ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D Surface - ์‚ฌ์‹ค์  ํ…์Šค์ฒ˜(Biome) ๋˜๋Š” ์œ„์„ฑ ์ด๋ฏธ์ง€ ์ ์šฉ
Args:
landform_type: 'river', 'coastal', 'glacial', 'volcanic', 'karst', 'arid' ์ค‘ ํ•˜๋‚˜.
None์ด๋ฉด ๊ธฐ์กด ๋กœ์ง ์‚ฌ์šฉ.
'glacial'๋งŒ ๋งŒ๋…„์„ค ํ‘œ์‹œ, ๋‚˜๋จธ์ง€๋Š” ๋ฌผ/ํ’€/์•”์„๋งŒ
"""
h, w = elevation.shape
x = np.arange(w)
y = np.arange(h)
# 1. ์ง€ํ˜• ํ…์Šค์ฒ˜๋ง (Biome Calculation)
# ๊ฒฝ์‚ฌ๋„ ๊ณ„์‚ฐ
dy, dx = np.gradient(elevation)
slope = np.sqrt(dx**2 + dy**2)
# Biome Index (0: ๋ฌผ/๋ชจ๋ž˜, 1: ํ’€, 2: ์•”์„, 3: ๋ˆˆ)
biome = np.zeros_like(elevation)
# ๊ธฐ๋ณธ: ํ’€ (Grass)
biome[:] = 1
# ๋ชจ๋ž˜/ํ‡ด์ ๋ฌผ (๋ฌผ ๊ทผ์ฒ˜ ๋‚ฎ์€ ๊ณณ + ํ‰ํƒ„ํ•œ ๊ณณ)
# add_water๊ฐ€ False์—ฌ๋„ ๊ณจ์งœ๊ธฐ(๋‚ฎ์€ ๊ณณ)๋Š” ํ‡ด์ ๋ฌผ์ด๋ฏ€๋กœ ๋ชจ๋ž˜์ƒ‰ ์ ์šฉ
sand_level = water_level + 5 if add_water else elevation.min() + 10
# ํ‡ด์ ์ง€ ํŒ๋ณ„:
# 1) Explicit sediment grid provided (> 0.5m)
# 2) Or Geometric guess (low & flat)
is_deposit = np.zeros_like(elevation, dtype=bool)
if sediment_grid is not None:
is_deposit = (sediment_grid > 0.5)
else:
is_deposit = (elevation < sand_level) & (slope < 0.5)
biome[is_deposit] = 0
# ์•”์„ (๊ฒฝ์‚ฌ๊ฐ€ ๊ธ‰ํ•œ ๊ณณ) - ์ ˆ๋ฒฝ
biome[slope > 1.2] = 2
# ์ง€ํ˜• ์œ ํ˜•๋ณ„ ์ฒ˜๋ฆฌ
if landform_type == 'glacial':
# ๋น™ํ•˜ ์ง€ํ˜•: ์ „์ฒด์ ์œผ๋กœ ๋น™ํ•˜/๋ˆˆ ํ‘œ์‹œ (๋†’์€ ๊ณณ + U์ž๊ณก ๋ฐ”๋‹ฅ๋„)
biome[elevation > 50] = 3 # ๋น™ํ•˜ ์ง€ํ˜•์€ ์ „์ฒด์— ๋ˆˆ/๋น™ํ•˜
biome[slope > 1.5] = 2 # ๊ฐ€ํŒŒ๋ฅธ ์ ˆ๋ฒฝ๋งŒ ์•”์„
elif landform_type in ['river', 'coastal']:
# ํ•˜์ฒœ/ํ•ด์•ˆ: ๋ฌผ ์˜์—ญ ๋ช…์‹œ์  ํ‘œ์‹œ (biome=0์„ ๋ฌผ์ƒ‰์œผ๋กœ)
if water_depth_grid is not None:
is_water = water_depth_grid > 0.5
biome[is_water] = 0 # ๋ฌผ ์˜์—ญ
# ์Œ์ˆ˜ ๊ณ ๋„ = ๋ฐ”๋‹ค/ํ˜ธ์ˆ˜
biome[elevation < 0] = 0
elif landform_type == 'arid':
# ๊ฑด์กฐ: ์ „์ฒด ๋ชจ๋ž˜์ƒ‰
biome[slope < 0.8] = 0 # ํ‰ํƒ„ํ•œ ๊ณณ์€ ๋ชจ๋ž˜
# else: ๊ธฐ๋ณธ (ํ™”์‚ฐ, ์นด๋ฅด์ŠคํŠธ) - ๋งŒ๋…„์„ค ์—†์Œ, ํ’€/์•”์„๋งŒ
# ์กฐ๊ธˆ ๋” ์ž์—ฐ์Šค๋Ÿฝ๊ฒŒ: ๋…ธ์ด์ฆˆ ์ถ”๊ฐ€ (๊ฒฝ๊ณ„๋ฉด ๋ธ”๋ Œ๋”ฉ ํšจ๊ณผ ํ‰๋‚ด)
noise = np.random.normal(0, 0.2, elevation.shape)
biome_noisy = np.clip(biome + noise, 0, 3).round(2)
# ์ปค์Šคํ…€ ์ปฌ๋Ÿฌ์Šค์ผ€์ผ (Discrete)
# 0: Soil/Sand (Yellowish), 1: Grass (Green), 2: Rock (Gray), 3: Snow/Water
if landform_type == 'glacial':
# ๋น™ํ•˜ ์ง€ํ˜•: ๋ˆˆ/๋น™ํ•˜ ํ‘œ์‹œ
realistic_colorscale = [
[0.0, '#E6C288'], [0.25, '#E6C288'], # Sand/Soil
[0.25, '#556B2F'], [0.5, '#556B2F'], # Grass
[0.5, '#808080'], [0.75, '#808080'], # Rock
[0.75, '#E0FFFF'], [1.0, '#FFFFFF'] # Ice/Snow (๋ฐ์€ ์ฒญ๋ฐฑ์ƒ‰)
]
colorbar_labels = ["ํ‡ด์ (ๅœŸ)", "์‹์ƒ(่‰)", "์•”์„(ๅฒฉ)", "๋น™ํ•˜(ๆฐท)"]
elif landform_type in ['river', 'coastal']:
# ํ•˜์ฒœ/ํ•ด์•ˆ: ๋ฌผ ํ‘œ์‹œ (ํŒŒ๋ž€์ƒ‰)
realistic_colorscale = [
[0.0, '#4682B4'], [0.25, '#4682B4'], # Water (Steel Blue)
[0.25, '#556B2F'], [0.5, '#556B2F'], # Grass
[0.5, '#808080'], [0.75, '#808080'], # Rock
[0.75, '#D2B48C'], [1.0, '#D2B48C'] # Sand (๋ฐ์€ ๊ฐˆ์ƒ‰)
]
colorbar_labels = ["์ˆ˜์—ญ(ๆฐด)", "์‹์ƒ(่‰)", "์•”์„(ๅฒฉ)", "์‚ฌ์งˆ(็ ‚)"]
elif landform_type == 'arid':
# ๊ฑด์กฐ ์ง€ํ˜•: ์‚ฌ๋ง‰ ํ‘œ์‹œ (๊ฐˆ์ƒ‰/์ฃผํ™ฉ)
realistic_colorscale = [
[0.0, '#EDC9AF'], [0.25, '#EDC9AF'], # Desert Sand
[0.25, '#CD853F'], [0.5, '#CD853F'], # Brown
[0.5, '#808080'], [0.75, '#808080'], # Rock
[0.75, '#DAA520'], [1.0, '#DAA520'] # Gold Sand
]
colorbar_labels = ["์‚ฌ๋ง‰(็ ‚)", "์•”์งˆ(ๅท–)", "์•”์„(ๅฒฉ)", "๋ชจ๋ž˜(ๆฒ™)"]
else:
# ๊ธฐ๋ณธ (ํ™”์‚ฐ, ์นด๋ฅด์ŠคํŠธ ๋“ฑ)
realistic_colorscale = [
[0.0, '#E6C288'], [0.25, '#E6C288'],
[0.25, '#556B2F'], [0.5, '#556B2F'],
[0.5, '#808080'], [0.75, '#808080'],
[0.75, '#A0522D'], [1.0, '#A0522D'] # ๊ฐˆ์ƒ‰ (๋งŒ๋…„์„ค ์ œ๊ฑฐ)
]
colorbar_labels = ["ํ‡ด์ (ๅœŸ)", "์‹์ƒ(่‰)", "์•”์„(ๅฒฉ)", "ํ‘œํ† (ๅœŸ)"]
# ์ง€ํ˜• ๋…ธ์ด์ฆˆ (Fractal Roughness) - ์‹œ๊ฐ์  ๋””ํ…Œ์ผ ์ถ”๊ฐ€
visual_z = (elevation + np.random.normal(0, 0.2, elevation.shape)).round(2)
# ํ…์Šค์ฒ˜ ๋กœ์ง (์ด๋ฏธ์ง€ ๋งคํ•‘)
final_surface_color = biome_noisy
final_colorscale = realistic_colorscale
final_cmin = 0
final_cmax = 3
final_colorbar = dict(
title=dict(text="์ง€ํ‘œ ์ƒํƒœ", font=dict(color='white')),
tickvals=[0.37, 1.12, 1.87, 2.62],
ticktext=colorbar_labels,
tickfont=dict(color='white')
)
if texture_path and os.path.exists(texture_path):
try:
img = Image.open(texture_path).convert('L')
img = img.resize((elevation.shape[1], elevation.shape[0]))
img_array = np.array(img) / 255.0
final_surface_color = img_array
# ํ…์Šค์ฒ˜ ํ…Œ๋งˆ์— ๋”ฐ๋ฅธ ์ปฌ๋Ÿฌ๋งต ์„ค์ •
if "barchan" in texture_path or "arid" in str(texture_path):
# ์‚ฌ๋ง‰: ๊ฐˆ์ƒ‰ -> ๊ธˆ์ƒ‰
final_colorscale = [[0.0, '#8B4513'], [0.3, '#CD853F'], [0.6, '#DAA520'], [1.0, '#FFD700']]
elif "valley" in texture_path or "meander" in texture_path or "delta" in texture_path:
# ์ˆฒ/ํ•˜์ฒœ: ์ง™์€ ๋…น์ƒ‰ -> ์—ฐ๋‘์ƒ‰ -> ํ™์ƒ‰
final_colorscale = [[0.0, '#2F4F4F'], [0.4, '#556B2F'], [0.7, '#8FBC8F'], [1.0, '#D2B48C']]
elif "volcano" in texture_path:
# ํ™”์‚ฐ: ๊ฒ€์ • -> ํšŒ์ƒ‰ -> ๋ถ‰์€๊ธฐ
final_colorscale = [[0.0, '#000000'], [0.5, '#404040'], [0.8, '#696969'], [1.0, '#8B4513']]
elif "fjord" in texture_path:
# ํ”ผ์˜ค๋ฅด: ์ง™์€ ํŒŒ๋ž‘(๋ฌผ) -> ํšŒ์ƒ‰(์ ˆ๋ฒฝ) -> ํฐ์ƒ‰(๋ˆˆ)
final_colorscale = [[0.0, '#191970'], [0.4, '#708090'], [0.8, '#C0C0C0'], [1.0, '#FFFFFF']]
elif "karst" in texture_path:
# ์นด๋ฅด์ŠคํŠธ: ์ง„๋…น์ƒ‰(๋ด‰์šฐ๋ฆฌ) -> ์—ฐ๋…น์ƒ‰(๋“คํŒ)
final_colorscale = [[0.0, '#556B2F'], [0.4, '#228B22'], [0.7, '#8FBC8F'], [1.0, '#F5DEB3']]
elif "fan" in texture_path or "braided" in texture_path:
# ์„ ์ƒ์ง€/๋ง์ƒํ•˜์ฒœ: ํ™ฉํ† ์ƒ‰(๋ชจ๋ž˜) -> ๊ฐˆ์ƒ‰(์ž๊ฐˆ)
final_colorscale = [[0.0, '#D2B48C'], [0.4, '#BC8F8F'], [0.8, '#8B4513'], [1.0, '#A0522D']]
elif "glacier" in texture_path or "cirque" in texture_path:
# ๋น™ํ•˜: ํฐ์ƒ‰ -> ํšŒ์ƒ‰ -> ์ฒญํšŒ์ƒ‰
final_colorscale = [[0.0, '#F0F8FF'], [0.4, '#B0C4DE'], [0.7, '#778899'], [1.0, '#2F4F4F']]
elif "lava" in texture_path:
# ์šฉ์•”: ๊ฒ€์ • -> ์ง„ํšŒ์ƒ‰
final_colorscale = [[0.0, '#000000'], [0.5, '#2F4F4F'], [1.0, '#696969']]
else:
# ๊ธฐ๋ณธ: ํ‘๋ฐฑ
final_colorscale = 'Gray'
final_cmin = 0
final_cmax = 1
final_colorbar = dict(title="ํ…์Šค์ฒ˜ ๋ช…์•”")
except Exception as e:
print(f"Texture error: {e}")
# ============ 3D Plot ============
# ์กฐ๋ช… ํšจ๊ณผ
lighting_effects = dict(ambient=0.4, diffuse=0.5, roughness=0.9, specular=0.1, fresnel=0.2)
# 1. Terrain Surface
trace_terrain = go.Surface(
z=visual_z, x=x, y=y,
surfacecolor=final_surface_color,
colorscale=final_colorscale,
cmin=final_cmin, cmax=final_cmax,
colorbar=final_colorbar,
lighting=lighting_effects,
hoverinfo='z'
)
data = [trace_terrain]
# 2. Water Surface
# Case A: water_depth_grid (Variable water height for rivers)
if water_depth_grid is not None:
# Create water elevation: usually bedrock/sediment + depth
# We need base elevation. 'elevation' argument includes sediment.
# Filter: Only show water where depth > threshold
water_mask = water_depth_grid > 0.1
if np.any(water_mask):
# Water Surface Elevation
water_z = visual_z.copy()
# To avoid z-fighting, add depth. But visual_z is noisy.
# Use original elevation + depth
water_z = elevation + water_depth_grid
# Hide dry areas
water_z[~water_mask] = np.nan
trace_water = go.Surface(
z=water_z, x=x, y=y,
colorscale=[[0, 'rgba(30,144,255,0.7)'], [1, 'rgba(30,144,255,0.7)']], # DodgerBlue
showscale=False,
lighting=dict(ambient=0.6, diffuse=0.5, specular=0.8, roughness=0.1), # Glossy
hoverinfo='skip'
)
data.append(trace_water)
# Case B: Flat water_level (Sea/Lake)
elif add_water:
# ํ‰๋ฉด ๋ฐ”๋‹ค
water_z = np.ones_like(elevation) * water_level
# Only draw where water is above terrain? Or just draw flat plane?
# Drawing flat plane is standard for sea.
# But for aesthetic, maybe mask it? No, sea level is simpler.
trace_water = go.Surface(
z=water_z,
x=x, y=y,
hoverinfo='none',
lighting = dict(ambient=0.6, diffuse=0.6, specular=0.5)
)
data.append(trace_water)
# ๋ ˆ์ด์•„์›ƒ (์–ด๋‘์šด ํ…Œ๋งˆ)
# ๋ ˆ์ด์•„์›ƒ (์–ด๋‘์šด ํ…Œ๋งˆ)
fig = go.Figure(data=data)
# ๋ ˆ์ด์•„์›ƒ (์–ด๋‘์šด ํ…Œ๋งˆ)
fig.update_layout(
title=dict(text=title, font=dict(color='white', size=16)),
# [Fix 1] Interaction Persistence (Move to Top Level)
uirevision='terrain_viz',
scene=dict(
xaxis=dict(title='X (m)', backgroundcolor='#1a1a2e', gridcolor='#444', color='#cccccc'),
yaxis=dict(title='Y (m)', backgroundcolor='#1a1a2e', gridcolor='#444', color='#cccccc'),
zaxis=dict(title='Elevation', backgroundcolor='#1a1a2e', gridcolor='#444', color='#cccccc'),
bgcolor='#0e1117', #
# uirevision removed from here
# [Fix 2] Better Camera Angle (Isometric) - Optional
camera=dict(
eye=dict(x=1.6, y=-1.6, z=0.8), # Isometric-ish
center=dict(x=0, y=0, z=-0.2), # Look slightly down
up=dict(x=0, y=0, z=1)
) if force_camera else None,
# [Fix 3] Proportions
aspectmode='manual',
aspectratio=dict(x=1, y=1, z=0.35) # Z is flattened slightly for realism
),
paper_bgcolor='#0e1117',
plot_bgcolor='#0e1117',
height=700, # Taller
margin=dict(l=10, r=10, t=50, b=10),
# Remove modebar to prevent accidental resets? No, keep it.
)
return fig
def render_v_valley_3d(elevation, x_coords, title, depth):
"""V์ž๊ณก ์ „์šฉ 3D ๋ Œ๋”๋ง - ๋‹จ์ผ ์ƒ‰์ƒ(copper)"""
fig = plt.figure(figsize=(14, 8), facecolor='#1a1a2e')
ax1 = fig.add_subplot(121, projection='3d', facecolor='#1a1a2e')
ax2 = fig.add_subplot(122, facecolor='#1a1a2e')
h, w = elevation.shape
x = np.arange(w)
y = np.arange(h)
X, Y = np.meshgrid(x, y)
# ๋‹จ์ผ ์ƒ‰์ƒ (copper)
elev_norm = (elevation - elevation.min()) / (elevation.max() - elevation.min() + 0.01)
ax1.plot_surface(X, Y, elevation,
facecolors=cm.copper(elev_norm),
linewidth=0, antialiased=True, shade=True)
# ํ•˜์ฒœ (์–ด๋‘์šด ์ƒ‰์ƒ)
min_z = elevation.min()
water_level = min_z + 3
channel_mask = elevation < water_level
if np.any(channel_mask):
ax1.plot_surface(X, Y, np.where(channel_mask, water_level, np.nan),
color='#2C3E50', alpha=0.9, linewidth=0)
ax1.view_init(elev=45, azim=200)
ax1.set_xlabel('X', color='white')
ax1.set_ylabel('Y', color='white')
ax1.set_zlabel('๊ณ ๋„', color='white')
ax1.set_title('3D ์กฐ๊ฐ๋„', fontsize=12, fontweight='bold', color='white')
ax1.tick_params(colors='white')
# ๋‹จ๋ฉด๋„ (๊ฐˆ์ƒ‰ ๊ณ„์—ด ํ†ต์ผ)
mid = h // 2
z = elevation[mid, :]
brown_colors = ['#8B4513', '#A0522D', '#CD853F'] # ๊ฐˆ์ƒ‰ ๊ณ„์—ด
for i, (color, label) in enumerate(zip(brown_colors, ['ํ‘œ์ธต', '์ค‘๊ฐ„์ธต', 'ํ•˜์ธต'])):
ax2.fill_between(x_coords, z.min() - 80, z - i*3, color=color, alpha=0.8, label=label)
ax2.plot(x_coords, z, color='#D2691E', linewidth=3)
# ํ•˜์ฒœ
river_idx = np.argmin(z)
ax2.fill_between(x_coords[max(0,river_idx-5):min(w,river_idx+6)],
z[max(0,river_idx-5):min(w,river_idx+6)],
z.min()+3, color='#2C3E50', alpha=0.9, label='ํ•˜์ฒœ')
# ๊นŠ์ด
ax2.annotate('', xy=(x_coords[river_idx], z.max()-5),
xytext=(x_coords[river_idx], z[river_idx]+5),
arrowprops=dict(arrowstyle='<->', color='#FFA500', lw=2))
ax2.text(x_coords[river_idx]+30, (z.max()+z[river_idx])/2, f'{depth:.0f}m',
fontsize=14, color='#FFA500', fontweight='bold')
ax2.set_xlim(x_coords.min(), x_coords.max())
ax2.set_ylim(z.min()-50, z.max()+20)
ax2.set_xlabel('๊ฑฐ๋ฆฌ (m)', fontsize=11, color='white')
ax2.set_ylabel('๊ณ ๋„ (m)', fontsize=11, color='white')
ax2.set_title('ํšก๋‹จ๋ฉด', fontsize=12, fontweight='bold', color='white')
ax2.legend(loc='upper right', fontsize=9, facecolor='#1a1a2e', labelcolor='white')
ax2.tick_params(colors='white')
ax2.grid(True, alpha=0.2, color='white')
fig.suptitle(title, fontsize=14, fontweight='bold', y=1.02, color='white')
plt.tight_layout()
return fig
def render_meander_realistic(x, y, oxbow_lakes, sinuosity):
"""๊ณก๋ฅ˜ ํ•˜์ฒœ ๋ Œ๋”๋ง - ๊ฐˆ์ƒ‰ ๊ณ„์—ด ํ†ต์ผ"""
try:
fig, ax = plt.subplots(figsize=(14, 5), facecolor='#1a1a2e')
ax.set_facecolor('#1a1a2e')
# ๋ฒ”๋žŒ์› ๋ฐฐ๊ฒฝ (๊ฐˆ์ƒ‰ ๊ณ„์—ด)
ax.axhspan(y.min()-100, y.max()+100, color='#3D2914', alpha=0.6)
# ํ•˜์ฒœ (์ง„ํ•œ ๊ฐˆ์ƒ‰)
ax.fill_between(x, y - 5, y + 5, color='#8B4513', alpha=0.9)
ax.plot(x, y, color='#CD853F', linewidth=2)
# ํฌ์ธํŠธ๋ฐ” (๋ฐ์€ ๊ฐˆ์ƒ‰)
ddy = np.gradient(np.gradient(y))
for i in range(20, len(x)-20, 20):
if np.abs(ddy[i]) > 0.3:
offset = -np.sign(ddy[i]) * 15
ax.scatter(x[i], y[i] + offset, s=80, c='#D2691E',
alpha=0.8, marker='o', zorder=5, edgecolors='#8B4513')
# ์šฐ๊ฐํ˜ธ (์–ด๋‘์šด ์ƒ‰)
for lake_x, lake_y in oxbow_lakes:
if len(lake_x) > 3:
ax.fill(lake_x, lake_y, color='#2C3E50', alpha=0.8)
ax.set_xlim(x.min() - 50, x.max() + 50)
ax.set_ylim(y.min() - 80, y.max() + 80)
ax.set_xlabel('ํ•˜๋ฅ˜ ๋ฐฉํ–ฅ (m)', fontsize=11, color='white')
ax.set_ylabel('์ขŒ์šฐ ๋ณ€์œ„ (m)', fontsize=11, color='white')
ax.set_title(f'๊ณก๋ฅ˜ ํ•˜์ฒœ (๊ตด๊ณก๋„: {sinuosity:.2f})', fontsize=13, fontweight='bold', color='white')
ax.tick_params(colors='white')
ax.grid(True, alpha=0.15, color='white')
# ๋ฒ”๋ก€
from matplotlib.lines import Line2D
legend_elements = [
Line2D([0], [0], color='#8B4513', lw=6, label='ํ•˜์ฒœ'),
Line2D([0], [0], marker='o', color='w', markerfacecolor='#D2691E', markersize=10, label='ํฌ์ธํŠธ๋ฐ”'),
Line2D([0], [0], marker='s', color='w', markerfacecolor='#2C3E50', markersize=10, label='์šฐ๊ฐํ˜ธ'),
]
ax.legend(handles=legend_elements, loc='upper right', fontsize=9,
facecolor='#1a1a2e', labelcolor='white')
return fig
except Exception as e:
fig, ax = plt.subplots(figsize=(12, 4), facecolor='#1a1a2e')
ax.set_facecolor('#1a1a2e')
ax.plot(x, y, color='#CD853F', linewidth=3, label='ํ•˜์ฒœ')
ax.set_title(f'๊ณก๋ฅ˜ ํ•˜์ฒœ (๊ตด๊ณก๋„: {sinuosity:.2f})', color='white')
ax.legend(facecolor='#1a1a2e', labelcolor='white')
ax.tick_params(colors='white')
return fig
def render_v_valley_section(x, elevation, depth):
"""V์ž๊ณก ๋‹จ๋ฉด ์‚ฌ์‹ค์  ๋ Œ๋”๋ง"""
fig, ax = plt.subplots(figsize=(12, 5))
mid = len(elevation) // 2
z = elevation[mid, :]
# ์•”์„์ธต (์ธต๋ฆฌ ํ‘œํ˜„)
for i, (color, y_offset) in enumerate([
('#8B7355', 0), ('#A0522D', -20), ('#CD853F', -40), ('#D2691E', -60)
]):
z_layer = z - i * 5
ax.fill_between(x, z.min() - 100, z_layer, color=color, alpha=0.7)
# ํ˜„์žฌ ์ง€ํ˜•๋ฉด
ax.plot(x, z, 'k-', linewidth=3)
# ํ•˜์ฒœ
river_idx = np.argmin(z)
river_width = 30
river_x = x[max(0, river_idx-3):min(len(x), river_idx+4)]
river_z = z[max(0, river_idx-3):min(len(z), river_idx+4)]
ax.fill_between(river_x, river_z, river_z.min()+3, color='#4169E1', alpha=0.8)
# ๊นŠ์ด ํ™”์‚ดํ‘œ
ax.annotate('', xy=(x[river_idx], z.max()), xytext=(x[river_idx], z[river_idx]),
arrowprops=dict(arrowstyle='<->', color='red', lw=3))
ax.text(x[river_idx]+50, (z.max()+z[river_idx])/2, f'๊นŠ์ด\n{depth:.0f}m',
fontsize=14, color='red', fontweight='bold', ha='left')
ax.set_xlim(x.min(), x.max())
ax.set_ylim(z.min()-50, z.max()+20)
ax.set_xlabel('๊ฑฐ๋ฆฌ (m)', fontsize=12)
ax.set_ylabel('๊ณ ๋„ (m)', fontsize=12)
ax.set_title('V์ž๊ณก ํšก๋‹จ๋ฉด', fontsize=14, fontweight='bold')
ax.grid(True, alpha=0.3, linestyle='--')
# ๋ฒ”๋ก€
patches = [
mpatches.Patch(color='#8B7355', label='์•”์„์ธต 1'),
mpatches.Patch(color='#A0522D', label='์•”์„์ธต 2'),
mpatches.Patch(color='#4169E1', label='ํ•˜์ฒœ')
]
ax.legend(handles=patches, loc='upper right')
return fig
# ============ ์ด๋ก  ์„ค๋ช… ์นด๋“œ ============
def show_theory_card(theory_dict, selected):
"""์ด๋ก  ์„ค๋ช… ์นด๋“œ ํ‘œ์‹œ"""
info = theory_dict[selected]
st.markdown(f"""
<div class="theory-card">
<div class="theory-title">๐Ÿ“ {selected}</div>
<p><span class="formula">{info['formula']}</span></p>
<p>{info['description']}</p>
<p><b>์ฃผ์š” ํŒŒ๋ผ๋ฏธํ„ฐ:</b> {', '.join(info['params'])}</p>
</div>
""", unsafe_allow_html=True)
# ============ ๋ฉ”์ธ ์•ฑ ============
def main():
# ========== ์ตœ์ƒ๋‹จ: ์ œ์ž‘์ž ์ •๋ณด ==========
st.markdown("""
<div style='background: linear-gradient(90deg, #1565C0, #42A5F5); padding: 8px 15px; border-radius: 8px; margin-bottom: 10px;'>
<div style='display: flex; justify-content: space-between; align-items: center; color: white;'>
<span style='font-size: 0.9rem;'>๐ŸŒ <b>Geo-Lab AI</b> - ์ด์ƒ์  ์ง€ํ˜• ์‹œ๋ฎฌ๋ ˆ์ดํ„ฐ</span>
<span style='font-size: 0.8rem;'>์ œ์ž‘: 2025 ํ•œ๋ฐฑ๊ณ ๋“ฑํ•™๊ต ๊น€ํ•œ์†”T</span>
</div>
</div>
""", unsafe_allow_html=True)
st.markdown('<p class="main-header">๐ŸŒ Geo-Lab AI: ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ</p>', unsafe_allow_html=True)
st.markdown("_๊ต์‚ฌ๋ฅผ ์œ„ํ•œ ์ง€ํ˜• ํ˜•์„ฑ๊ณผ์ • ์‹œ๊ฐํ™” ๋„๊ตฌ_")
# ========== ๋ฐฉ๋ฌธ์ž ์นด์šดํ„ฐ (Session State) ==========
if 'visitor_count' not in st.session_state:
st.session_state.visitor_count = 1
if 'today_count' not in st.session_state:
st.session_state.today_count = 1
# ์ƒ๋‹จ ์˜ค๋ฅธ์ชฝ ๋ฐฉ๋ฌธ์ž ํ‘œ์‹œ
col_title, col_visitor = st.columns([4, 1])
with col_visitor:
st.markdown(f"""
<div style='text-align: right; font-size: 0.85rem; color: #666;'>
๐Ÿ‘ค ์˜ค๋Š˜: <b>{st.session_state.today_count}</b> |
์ด: <b>{st.session_state.visitor_count}</b>
</div>
""", unsafe_allow_html=True)
# ========== ์‚ฌ์ด๋“œ๋ฐ”: ๊ฐ€์ด๋“œ & ์—…๋ฐ์ดํŠธ ==========
st.sidebar.title("๐ŸŒ Geo-Lab AI")
# ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ
with st.sidebar.expander("๐Ÿ“š ์‚ฌ์šฉ์ž ๊ฐ€์ด๋“œ", expanded=False):
st.markdown("""
**๐ŸŽฏ ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ (๊ต์‚ฌ์šฉ)**
1. ์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ (ํ•˜์ฒœ, ๋น™ํ•˜, ํ™”์‚ฐ ๋“ฑ)
2. ์›ํ•˜๋Š” ์ง€ํ˜• ์„ ํƒ
3. 2D ํ‰๋ฉด๋„ ํ™•์ธ
4. "๐Ÿ”ฒ 3D ๋ทฐ ๋ณด๊ธฐ" ํด๋ฆญํ•˜์—ฌ 3D ํ™•์ธ
5. **โฌ‡๏ธ ์•„๋ž˜๋กœ ์Šคํฌ๋กคํ•˜๋ฉด ํ˜•์„ฑ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜!**
**๐Ÿ’ก ํŒ**
- ์Šฌ๋ผ์ด๋”๋กœ ํ˜•์„ฑ๋‹จ๊ณ„ ์กฐ์ ˆ (0%โ†’100%)
- ์ž๋™์žฌ์ƒ ๋ฒ„ํŠผ์œผ๋กœ ์• ๋‹ˆ๋ฉ”์ด์…˜ ์‹คํ–‰
""")
# ์—…๋ฐ์ดํŠธ ๋‚ด์—ญ
with st.sidebar.expander("๐Ÿ“‹ ์—…๋ฐ์ดํŠธ ๋‚ด์—ญ", expanded=False):
st.markdown("""
**v4.1 (2025-12-14)** ๐Ÿ†•
- ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ 31์ข… ์ถ”๊ฐ€
- ํ˜•์„ฑ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ ๊ธฐ๋Šฅ
- 7๊ฐœ ์นดํ…Œ๊ณ ๋ฆฌ ๋ถ„๋ฅ˜
**v4.0**
- Project Genesis ํ†ตํ•ฉ ๋ฌผ๋ฆฌ ์—”์ง„
- ์ง€ํ˜• ์‹œ๋‚˜๋ฆฌ์˜ค ํƒญ
**v3.0**
- ๋‹ค์ค‘ ์ด๋ก  ๋ชจ๋ธ ๋น„๊ต
- ์Šคํฌ๋ฆฝํŠธ ๋žฉ
""")
st.sidebar.markdown("---")
# Resolution Control
grid_size = st.sidebar.slider("ํ•ด์ƒ๋„ (Grid Size)", 40, 150, 60, 10, help="๋‚ฎ์„์ˆ˜๋ก ๋น ๋ฆ„ / ๋†’์„์ˆ˜๋ก ์ •๋ฐ€")
# ========== ํƒญ ์žฌ๋ฐฐ์น˜: ๊ฐค๋Ÿฌ๋ฆฌ ๋จผ์ € ==========
t_gallery, t_genesis, t_scenarios, t_lab = st.tabs([
"๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ",
"๐ŸŒ Project Genesis (์‹œ๋ฎฌ๋ ˆ์ด์…˜)",
"๐Ÿ“š ์ง€ํ˜• ์‹œ๋‚˜๋ฆฌ์˜ค (Landforms)",
"๐Ÿ’ป ์Šคํฌ๋ฆฝํŠธ ๋žฉ (Lab)"
])
# 1. Alias for Genesis Main Tab
tab_genesis = t_genesis
# 2. Ideal Landform Gallery (FIRST TAB - ๊ต์‚ฌ์šฉ ๋ฉ”์ธ)
with t_gallery:
st.header("๐Ÿ“– ์ด์ƒ์  ์ง€ํ˜• ๊ฐค๋Ÿฌ๋ฆฌ")
st.markdown("_๊ต๊ณผ์„œ์ ์ธ ์ง€ํ˜• ํ˜•ํƒœ๋ฅผ ๊ธฐํ•˜ํ•™์  ๋ชจ๋ธ๋กœ ์‹œ๊ฐํ™”ํ•ฉ๋‹ˆ๋‹ค._")
# ๊ฐ•์กฐ ๋ฉ”์‹œ์ง€
st.info("๐Ÿ’ก **Tip:** ์ง€ํ˜• ์„ ํƒ ํ›„ **์•„๋ž˜๋กœ ์Šคํฌ๋กค**ํ•˜๋ฉด **๐ŸŽฌ ํ˜•์„ฑ ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜**์„ ํ™•์ธํ•  ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค!")
# ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์ง€ํ˜•
st.sidebar.markdown("---")
st.sidebar.subheader("๐Ÿ—‚๏ธ ์ง€ํ˜• ์นดํ…Œ๊ณ ๋ฆฌ")
category = st.sidebar.radio("์นดํ…Œ๊ณ ๋ฆฌ ์„ ํƒ", [
"๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•",
"๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•",
"โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•",
"๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•",
"๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•",
"๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•",
"๐Ÿ–๏ธ ํ•ด์•ˆ ์ง€ํ˜•"
], key="gallery_cat")
# ์นดํ…Œ๊ณ ๋ฆฌ๋ณ„ ์˜ต์…˜
if category == "๐ŸŒŠ ํ•˜์ฒœ ์ง€ํ˜•":
landform_options = {
"๐Ÿ“ ์„ ์ƒ์ง€ (Alluvial Fan)": "alluvial_fan",
"๐Ÿ ์ž์œ ๊ณก๋ฅ˜ (Free Meander)": "free_meander",
"โ›ฐ๏ธ ๊ฐ์ž…๊ณก๋ฅ˜+ํ•˜์•ˆ๋‹จ๊ตฌ (Incised Meander)": "incised_meander",
"๐Ÿ”๏ธ V์ž๊ณก (V-Valley)": "v_valley",
"๐ŸŒŠ ๋ง์ƒํ•˜์ฒœ (Braided River)": "braided_river",
"๐Ÿ’ง ํญํฌ (Waterfall)": "waterfall",
}
elif category == "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ ์œ ํ˜•":
landform_options = {
"๐Ÿ”บ ์ผ๋ฐ˜ ์‚ผ๊ฐ์ฃผ (Delta)": "delta",
"๐Ÿฆถ ์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ (Bird-foot)": "bird_foot_delta",
"๐ŸŒ™ ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ (Arcuate)": "arcuate_delta",
"๐Ÿ“ ์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ (Cuspate)": "cuspate_delta",
}
elif category == "โ„๏ธ ๋น™ํ•˜ ์ง€ํ˜•":
landform_options = {
"โ„๏ธ U์ž๊ณก (U-Valley)": "u_valley",
"๐Ÿฅฃ ๊ถŒ๊ณก (Cirque)": "cirque",
"๐Ÿ”๏ธ ํ˜ธ๋ฅธ (Horn)": "horn",
"๐ŸŒŠ ํ”ผ์˜ค๋ฅด๋“œ (Fjord)": "fjord",
"๐Ÿฅš ๋“œ๋Ÿผ๋ฆฐ (Drumlin)": "drumlin",
"๐Ÿชจ ๋น™ํ‡ด์„ (Moraine)": "moraine",
}
elif category == "๐ŸŒ‹ ํ™”์‚ฐ ์ง€ํ˜•":
landform_options = {
"๐Ÿ›ก๏ธ ์ˆœ์ƒํ™”์‚ฐ (Shield)": "shield_volcano",
"๐Ÿ—ป ์„ฑ์ธตํ™”์‚ฐ (Stratovolcano)": "stratovolcano",
"๐Ÿ•ณ๏ธ ์นผ๋ฐ๋ผ (Caldera)": "caldera",
"๐Ÿ’ง ํ™”๊ตฌํ˜ธ (Crater Lake)": "crater_lake",
"๐ŸŸซ ์šฉ์•”๋Œ€์ง€ (Lava Plateau)": "lava_plateau",
}
elif category == "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•":
landform_options = {
"๐Ÿ•ณ๏ธ ๋Œ๋ฆฌ๋„ค (Doline)": "karst_doline",
}
elif category == "๐Ÿœ๏ธ ๊ฑด์กฐ ์ง€ํ˜•":
landform_options = {
"๐Ÿœ๏ธ ๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ (Barchan)": "barchan",
"๐Ÿ—ฟ ๋ฉ”์‚ฌ/๋ทฐํŠธ (Mesa/Butte)": "mesa_butte",
}
else: # ํ•ด์•ˆ ์ง€ํ˜•
landform_options = {
"๐Ÿ–๏ธ ํ•ด์•ˆ ์ ˆ๋ฒฝ (Coastal Cliff)": "coastal_cliff",
"๐ŸŒŠ ์‚ฌ์ทจ+์„ํ˜ธ (Spit+Lagoon)": "spit_lagoon",
"๐Ÿ๏ธ ์œก๊ณ„์‚ฌ์ฃผ (Tombolo)": "tombolo",
"๐ŸŒ€ ๋ฆฌ์•„์Šค ํ•ด์•ˆ (Ria Coast)": "ria_coast",
"๐ŸŒ‰ ํ•ด์‹์•„์น˜ (Sea Arch)": "sea_arch",
"๐Ÿ–๏ธ ํ•ด์•ˆ์‚ฌ๊ตฌ (Coastal Dune)": "coastal_dune",
}
col_sel, col_view = st.columns([1, 3])
with col_sel:
selected_landform = st.selectbox("์ง€ํ˜• ์„ ํƒ", list(landform_options.keys()))
landform_key = landform_options[selected_landform]
# Parameters based on landform type
st.markdown("---")
st.subheader("โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
gallery_grid_size = st.slider("ํ•ด์ƒ๋„", 50, 150, 80, 10, key="gallery_res")
# ๋™์  ์ง€ํ˜• ์ƒ์„ฑ (IDEAL_LANDFORM_GENERATORS ์‚ฌ์šฉ)
if landform_key in IDEAL_LANDFORM_GENERATORS:
generator = IDEAL_LANDFORM_GENERATORS[landform_key]
# lambda์ธ ๊ฒฝ์šฐ grid_size๋งŒ ์ „๋‹ฌ
try:
elevation = generator(gallery_grid_size)
except TypeError:
# stage ์ธ์ž๊ฐ€ ํ•„์š”ํ•œ ๊ฒฝ์šฐ
elevation = generator(gallery_grid_size, 1.0)
else:
st.error(f"์ง€ํ˜• '{landform_key}' ์ƒ์„ฑ๊ธฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
elevation = np.zeros((gallery_grid_size, gallery_grid_size))
with col_view:
# ๊ธฐ๋ณธ: 2D ํ‰๋ฉด๋„ (matplotlib) - WebGL ์ปจํ…์ŠคํŠธ ์‚ฌ์šฉ ์•ˆ ํ•จ
import matplotlib.pyplot as plt
import matplotlib.colors as mcolors
fig_2d, ax = plt.subplots(figsize=(8, 8))
# ์ง€ํ˜• ์ƒ‰์ƒ ๋งต
cmap = plt.cm.terrain
# ๋ฌผ์ด ์žˆ๋Š” ์ง€ํ˜•์€ ํŒŒ๋ž€์ƒ‰ ์˜ค๋ฒ„๋ ˆ์ด
water_mask = elevation < 0
im = ax.imshow(elevation, cmap=cmap, origin='upper')
# ๋ฌผ ์˜์—ญ ํ‘œ์‹œ
if water_mask.any():
water_overlay = np.ma.masked_where(~water_mask, np.ones_like(elevation))
ax.imshow(water_overlay, cmap='Blues', alpha=0.6, origin='upper')
ax.set_title(f"{selected_landform}", fontsize=14)
ax.axis('off')
# ์ปฌ๋Ÿฌ๋ฐ”
cbar = plt.colorbar(im, ax=ax, shrink=0.6, label='๊ณ ๋„ (m)')
st.pyplot(fig_2d)
plt.close(fig_2d)
# 3D ๋ณด๊ธฐ (๋ฒ„ํŠผ ํด๋ฆญ ์‹œ์—๋งŒ)
if st.button("๐Ÿ”ฒ 3D ๋ทฐ ๋ณด๊ธฐ", key="show_3d_view"):
fig_3d = render_terrain_plotly(
elevation,
f"{selected_landform} - 3D",
add_water=(landform_key in ["delta", "meander", "coastal_cliff", "fjord", "ria_coast", "spit_lagoon"]),
water_level=0 if landform_key in ["delta", "coastal_cliff"] else -999,
force_camera=True
)
st.plotly_chart(fig_3d, use_container_width=True)
# Educational Description
descriptions = {
# ํ•˜์ฒœ ์ง€ํ˜•
"delta": "**์‚ผ๊ฐ์ฃผ**: ํ•˜์ฒœ์ด ๋ฐ”๋‹ค๋‚˜ ํ˜ธ์ˆ˜์— ์œ ์ž…๋  ๋•Œ ์œ ์†์ด ๊ฐ์†Œํ•˜์—ฌ ์šด๋ฐ˜ ์ค‘์ด๋˜ ํ‡ด์ ๋ฌผ์ด ์Œ“์—ฌ ํ˜•์„ฑ๋ฉ๋‹ˆ๋‹ค.",
"alluvial_fan": "**์„ ์ƒ์ง€**: ์‚ฐ์ง€์—์„œ ํ‰์ง€๋กœ ๋‚˜์˜ค๋Š” ๊ณณ์—์„œ ๊ฒฝ์‚ฌ๊ฐ€ ๊ธ‰๊ฐํ•˜์—ฌ ์šด๋ฐ˜๋ ฅ์ด ์ค„์–ด๋“ค๋ฉด์„œ ํ‡ด์ ๋ฌผ์ด ๋ถ€์ฑ„๊ผด๋กœ ์Œ“์ž…๋‹ˆ๋‹ค.",
"meander": "**๊ณก๋ฅ˜**: ํ•˜์ฒœ์ด ์ค‘๋ฅ˜์—์„œ ์ธก๋ฐฉ ์นจ์‹๊ณผ ํ‡ด์ ์„ ๋ฐ˜๋ณตํ•˜๋ฉฐ S์ž ํ˜•ํƒœ๋กœ ์‚ฌํ–‰ํ•ฉ๋‹ˆ๋‹ค.",
"free_meander": "**์ž์œ ๊ณก๋ฅ˜**: ๋ฒ”๋žŒ์› ์œ„๋ฅผ ์ž์œ ๋กญ๊ฒŒ ์‚ฌํ–‰ํ•˜๋Š” ๊ณก๋ฅ˜. ์ž์—ฐ์ œ๋ฐฉ(Levee)๊ณผ ๋ฐฐํ›„์Šต์ง€๊ฐ€ ํŠน์ง•์ž…๋‹ˆ๋‹ค.",
"incised_meander": "**๊ฐ์ž…๊ณก๋ฅ˜**: ์œต๊ธฐ๋กœ ์ธํ•ด ๊ณก๋ฅ˜๊ฐ€ ๊ธฐ๋ฐ˜์•”์„ ํŒŒ๊ณ ๋“ค๋ฉด์„œ ํ˜•์„ฑ. ํ•˜์•ˆ๋‹จ๊ตฌ(River Terrace)๊ฐ€ ํ•จ๊ป˜ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.",
"v_valley": "**V์ž๊ณก**: ํ•˜์ฒœ์˜ ํ•˜๋ฐฉ ์นจ์‹์ด ์šฐ์„ธํ•˜๊ฒŒ ์ž‘์šฉํ•˜์—ฌ ํ˜•์„ฑ๋œ V์ž ๋‹จ๋ฉด์˜ ๊ณจ์งœ๊ธฐ.",
# ์‚ผ๊ฐ์ฃผ ์œ ํ˜•
"bird_foot_delta": "**์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ**: ๋ฏธ์‹œ์‹œํ”ผ๊ฐ•ํ˜•. ํŒŒ๋ž‘ ์•ฝํ•˜๊ณ  ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰ ๋งŽ์„ ๋•Œ ์ƒˆ๋ฐœ ๋ชจ์–‘์œผ๋กœ ๊ธธ๊ฒŒ ๋ป—์Šต๋‹ˆ๋‹ค.",
"arcuate_delta": "**ํ˜ธ์ƒ ์‚ผ๊ฐ์ฃผ**: ๋‚˜์ผ๊ฐ•ํ˜•. ํŒŒ๋ž‘๊ณผ ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰์ด ๊ท ํ˜•์„ ์ด๋ฃจ์–ด ๋ถ€๋“œ๋Ÿฌ์šด ํ˜ธ(Arc) ํ˜•ํƒœ.",
"cuspate_delta": "**์ฒจ๋‘์ƒ ์‚ผ๊ฐ์ฃผ**: ํ‹ฐ๋ฒ ๋ฅด๊ฐ•ํ˜•. ํŒŒ๋ž‘์ด ๊ฐ•ํ•ด ์‚ผ๊ฐ์ฃผ๊ฐ€ ๋พฐ์กฑํ•œ ํ™”์‚ด์ด‰ ๋ชจ์–‘์œผ๋กœ ํ˜•์„ฑ.",
# ๋น™ํ•˜ ์ง€ํ˜•
"u_valley": "**U์ž๊ณก**: ๋น™ํ•˜์˜ ์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ U์ž ๋‹จ๋ฉด์˜ ๊ณจ์งœ๊ธฐ. ์ธก๋ฒฝ์ด ๊ธ‰ํ•˜๊ณ  ๋ฐ”๋‹ฅ์ด ํ‰ํƒ„ํ•ฉ๋‹ˆ๋‹ค.",
"cirque": "**๊ถŒ๊ณก(Cirque)**: ๋น™ํ•˜์˜ ์‹œ์ž‘์ . ๋ฐ˜์›ํ˜• ์›€ํ‘น ํŒŒ์ธ ์ง€ํ˜•์œผ๋กœ, ๋น™ํ•˜ ์œตํ•ด ํ›„ ํ˜ธ์ˆ˜(Tarn)๊ฐ€ ํ˜•์„ฑ๋ฉ๋‹ˆ๋‹ค.",
"horn": "**ํ˜ธ๋ฅธ(Horn)**: ์—ฌ๋Ÿฌ ๊ถŒ๊ณก์ด ๋งŒ๋‚˜๋Š” ๊ณณ์—์„œ ์นจ์‹๋˜์ง€ ์•Š๊ณ  ๋‚จ์€ ๋พฐ์กฑํ•œ ํ”ผ๋ผ๋ฏธ๋“œํ˜• ๋ด‰์šฐ๋ฆฌ. (์˜ˆ: ๋งˆํ„ฐํ˜ธ๋ฅธ)",
"fjord": "**ํ”ผ์˜ค๋ฅด๋“œ(Fjord)**: ๋น™ํ•˜๊ฐ€ ํŒŒ๋‚ธ U์ž๊ณก์— ๋ฐ”๋‹ค๊ฐ€ ์œ ์ž…๋œ ์ข๊ณ  ๊นŠ์€ ๋งŒ. (์˜ˆ: ๋…ธ๋ฅด์›จ์ด)",
"drumlin": "**๋“œ๋Ÿผ๋ฆฐ(Drumlin)**: ๋น™ํ•˜ ํ‡ด์ ๋ฌผ์ด ๋น™ํ•˜ ํ๋ฆ„ ๋ฐฉํ–ฅ์œผ๋กœ ๊ธธ์ญ‰ํ•˜๊ฒŒ ์Œ“์ธ ํƒ€์›ํ˜• ์–ธ๋•.",
"moraine": "**๋น™ํ‡ด์„(Moraine)**: ๋น™ํ•˜๊ฐ€ ์šด๋ฐ˜ํ•œ ์•”์„ค์ด ํ‡ด์ ๋œ ์ง€ํ˜•. ์ธกํ‡ด์„, ์ข…ํ‡ด์„ ๋“ฑ์ด ์žˆ์Šต๋‹ˆ๋‹ค.",
# ํ™”์‚ฐ ์ง€ํ˜•
"shield_volcano": "**์ˆœ์ƒํ™”์‚ฐ**: ์œ ๋™์„ฑ ๋†’์€ ํ˜„๋ฌด์•”์งˆ ์šฉ์•”์ด ์™„๋งŒํ•˜๊ฒŒ(5-10ยฐ) ์Œ“์—ฌ ๋ฐฉํŒจ ํ˜•ํƒœ. (์˜ˆ: ํ•˜์™€์ด ๋งˆ์šฐ๋‚˜๋กœ์•„)",
"stratovolcano": "**์„ฑ์ธตํ™”์‚ฐ**: ์šฉ์•”๊ณผ ํ™”์‚ฐ์‡„์„ค๋ฌผ์ด ๊ต๋Œ€๋กœ ์Œ“์—ฌ ๊ธ‰ํ•œ(25-35ยฐ) ์›๋ฟ”ํ˜•. (์˜ˆ: ํ›„์ง€์‚ฐ, ๋ฐฑ๋‘์‚ฐ)",
"caldera": "**์นผ๋ฐ๋ผ**: ๋Œ€๊ทœ๋ชจ ๋ถ„ํ™” ํ›„ ๋งˆ๊ทธ๋งˆ๋ฐฉ ํ•จ๋ชฐ๋กœ ํ˜•์„ฑ๋œ ๊ฑฐ๋Œ€ํ•œ ๋ถ„์ง€. (์˜ˆ: ๋ฐฑ๋‘์‚ฐ ์ฒœ์ง€)",
"crater_lake": "**ํ™”๊ตฌํ˜ธ**: ํ™”๊ตฌ๋‚˜ ์นผ๋ฐ๋ผ์— ๋ฌผ์ด ๊ณ ์—ฌ ํ˜•์„ฑ๋œ ํ˜ธ์ˆ˜. (์˜ˆ: ๋ฐฑ๋‘์‚ฐ ์ฒœ์ง€)",
"lava_plateau": "**์šฉ์•”๋Œ€์ง€**: ์—ด๊ทน ๋ถ„์ถœ๋กœ ํ˜„๋ฌด์•”์งˆ ์šฉ์•”์ด ๋„“๊ฒŒ ํŽผ์ณ์ ธ ํ‰ํƒ„ํ•œ ๋Œ€์ง€ ํ˜•์„ฑ.",
# ๊ฑด์กฐ ์ง€ํ˜•
"barchan": "**๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ**: ๋ฐ”๋žŒ์ด ํ•œ ๋ฐฉํ–ฅ์—์„œ ๋ถˆ ๋•Œ ํ˜•์„ฑ๋˜๋Š” ์ดˆ์Šน๋‹ฌ ๋ชจ์–‘์˜ ์‚ฌ๊ตฌ.",
"mesa_butte": "**๋ฉ”์‚ฌ/๋ทฐํŠธ**: ์ฐจ๋ณ„์นจ์‹์œผ๋กœ ๋‚จ์€ ํƒ์ƒ์ง€. ๋ฉ”์‚ฌ๋Š” ํฌ๊ณ  ํ‰ํƒ„, ๋ทฐํŠธ๋Š” ์ž‘๊ณ  ๋†’์Šต๋‹ˆ๋‹ค.",
"karst_doline": "**๋Œ๋ฆฌ๋„ค(Doline)**: ์„ํšŒ์•” ์šฉ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ์›€ํ‘น ํŒŒ์ธ ์™€์ง€. ์นด๋ฅด์ŠคํŠธ ์ง€ํ˜•์˜ ๋Œ€ํ‘œ์  ํŠน์ง•.",
# ํ•ด์•ˆ ์ง€ํ˜•
"coastal_cliff": "**ํ•ด์•ˆ ์ ˆ๋ฒฝ**: ํŒŒ๋ž‘์˜ ์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ์ ˆ๋ฒฝ. ์ ˆ๋ฒฝ ํ›„ํ‡ด ์‹œ ์‹œ์Šคํƒ(Sea Stack)์ด ๋‚จ๊ธฐ๋„ ํ•ฉ๋‹ˆ๋‹ค.",
"spit_lagoon": "**์‚ฌ์ทจ+์„ํ˜ธ**: ์—ฐ์•ˆ๋ฅ˜์— ์˜ํ•ด ํ‡ด์ ๋ฌผ์ด ๊ธธ๊ฒŒ ์Œ“์ธ ์‚ฌ์ทจ๊ฐ€ ๋งŒ์„ ๋ง‰์•„ ์„ํ˜ธ(Lagoon)๋ฅผ ํ˜•์„ฑํ•ฉ๋‹ˆ๋‹ค.",
"tombolo": "**์œก๊ณ„์‚ฌ์ฃผ(Tombolo)**: ์—ฐ์•ˆ๋ฅ˜์— ์˜ํ•œ ํ‡ด์ ์œผ๋กœ ์œก์ง€์™€ ์„ฌ์ด ๋ชจ๋ž˜ํ†ฑ์œผ๋กœ ์—ฐ๊ฒฐ๋œ ์ง€ํ˜•.",
"ria_coast": "**๋ฆฌ์•„์Šค์‹ ํ•ด์•ˆ**: ๊ณผ๊ฑฐ ํ•˜๊ณก์ด ํ•ด์ˆ˜๋ฉด ์ƒ์Šน์œผ๋กœ ์นจ์ˆ˜๋˜์–ด ํ˜•์„ฑ๋œ ํ†ฑ๋‹ˆ ๋ชจ์–‘ ํ•ด์•ˆ์„ .",
"sea_arch": "**ํ•ด์‹์•„์น˜(Sea Arch)**: ๊ณถ์—์„œ ํŒŒ๋ž‘ ์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ์•„์น˜ํ˜• ์ง€ํ˜•. ๋” ์ง„ํ–‰๋˜๋ฉด ์‹œ์Šคํƒ์ด ๋ฉ๋‹ˆ๋‹ค.",
"coastal_dune": "**ํ•ด์•ˆ์‚ฌ๊ตฌ**: ํ•ด๋นˆ์˜ ๋ชจ๋ž˜๊ฐ€ ๋ฐ”๋žŒ์— ์˜ํ•ด ์œก์ง€ ์ชฝ์œผ๋กœ ์šด๋ฐ˜๋˜์–ด ํ˜•์„ฑ๋œ ๋ชจ๋ž˜ ์–ธ๋•.",
# ํ•˜์ฒœ ์ถ”๊ฐ€
"braided_river": "**๋ง์ƒํ•˜์ฒœ(Braided River)**: ํ‡ด์ ๋ฌผ์ด ๋งŽ๊ณ  ๊ฒฝ์‚ฌ๊ฐ€ ๊ธ‰ํ•  ๋•Œ ์—ฌ๋Ÿฌ ์ˆ˜๋กœ๊ฐ€ ๊ฐˆ๋ผ์กŒ๋‹ค ํ•ฉ์ณ์ง€๋Š” ํ•˜์ฒœ.",
"waterfall": "**ํญํฌ(Waterfall)**: ๊ฒฝ์•”๊ณผ ์—ฐ์•”์˜ ์ฐจ๋ณ„์นจ์‹์œผ๋กœ ํ˜•์„ฑ๋œ ๊ธ‰๊ฒฝ์‚ฌ ๋‚™์ฐจ. ํ›„ํ‡ดํ•˜๋ฉฐ ํ˜‘๊ณก ํ˜•์„ฑ.",
}
st.info(descriptions.get(landform_key, "์„ค๋ช… ์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค."))
# ํ˜•์„ฑ๊ณผ์ • ์• ๋‹ˆ๋ฉ”์ด์…˜ (์ง€์› ์ง€ํ˜•๋งŒ)
if landform_key in ANIMATED_LANDFORM_GENERATORS:
st.markdown("---")
st.subheader("๐ŸŽฌ ํ˜•์„ฑ ๊ณผ์ •")
# ๋‹จ์ผ ์Šฌ๋ผ์ด๋”๋กœ ํ˜•์„ฑ ๋‹จ๊ณ„ ์กฐ์ ˆ
stage_value = st.slider(
"ํ˜•์„ฑ ๋‹จ๊ณ„ (0% = ์‹œ์ž‘, 100% = ์™„์„ฑ)",
0.0, 1.0, 1.0, 0.05,
key="gallery_stage_slider"
)
# ํ•ด๋‹น ๋‹จ๊ณ„ ์ง€ํ˜• ์ƒ์„ฑ
anim_func = ANIMATED_LANDFORM_GENERATORS[landform_key]
stage_elev = anim_func(gallery_grid_size, stage_value)
# ๋ฌผ ์ƒ์„ฑ
stage_water = np.maximum(0, -stage_elev + 1.0)
stage_water[stage_elev > 2] = 0
# ํŠน์ • ์ง€ํ˜• ๋ฌผ ์ฒ˜๋ฆฌ
if landform_key == "alluvial_fan":
apex_y = int(gallery_grid_size * 0.15)
center = gallery_grid_size // 2
for r in range(apex_y + 5):
for dc in range(-2, 3):
c = center + dc
if 0 <= c < gallery_grid_size:
stage_water[r, c] = 3.0
# ๋‹จ์ผ 3D ๋ Œ๋”๋ง (WebGL ์ปจํ…์ŠคํŠธ ์ ˆ์•ฝ)
fig_stage = render_terrain_plotly(
stage_elev,
f"{selected_landform} - {int(stage_value*100)}%",
add_water=True,
water_depth_grid=stage_water,
water_level=-999,
force_camera=True
)
st.plotly_chart(fig_stage, use_container_width=True, key="stage_view")
# ์ž๋™ ์žฌ์ƒ ๋ฒ„ํŠผ
if st.button("โ–ถ๏ธ ์ž๋™ ์žฌ์ƒ (0%โ†’100%)", key="auto_play"):
stage_container = st.empty()
prog = st.progress(0)
for i in range(11):
s = i / 10.0
elev = anim_func(gallery_grid_size, s)
water = np.maximum(0, -elev + 1.0)
water[elev > 2] = 0
fig = render_terrain_plotly(
elev, f"{selected_landform} - {int(s*100)}%",
add_water=True, water_depth_grid=water,
water_level=-999, force_camera=False
)
stage_container.plotly_chart(fig, use_container_width=True)
prog.progress(s)
import time
time.sleep(0.4)
st.success("โœ… ์™„๋ฃŒ!")
# 3. Scenarios Sub-tabs
with t_scenarios:
tab_river, tab_coast, tab_karst, tab_volcano, tab_glacial, tab_arid, tab_plain = st.tabs([
"๐ŸŒŠ ํ•˜์ฒœ", "๐Ÿ–๏ธ ํ•ด์•ˆ", "๐Ÿฆ‡ ์นด๋ฅด์ŠคํŠธ", "๐ŸŒ‹ ํ™”์‚ฐ", "โ„๏ธ ๋น™ํ•˜", "๐Ÿœ๏ธ ๊ฑด์กฐ", "๐ŸŒพ ํ‰์•ผ"
])
# 4. Lab Tab Alias
tab_script = t_lab
# ===== ํ•˜์ฒœ ์ง€ํ˜• (ํ†ตํ•ฉ) =====
with tab_river:
# ํ•˜์ฒœ ์„ธ๋ถ€ ํƒญ
river_sub = st.tabs(["๐Ÿ”๏ธ V์ž๊ณก/ํ˜‘๊ณก", "๐Ÿ ๊ณก๋ฅ˜/์šฐ๊ฐํ˜ธ", "๐Ÿ”บ ์‚ผ๊ฐ์ฃผ", "๐Ÿ“ ์„ ์ƒ์ง€", "๐Ÿ“Š ํ•˜์•ˆ๋‹จ๊ตฌ", "โš”๏ธ ํ•˜์ฒœ์Ÿํƒˆ", "๐Ÿ”„ ๊ฐ์ž…๊ณก๋ฅ˜", "๐ŸŒŠ ๋ง์ƒํ•˜์ฒœ", "๐Ÿ’ง ํญํฌ/ํฌํŠธํ™€", "๐ŸŒพ ๋ฒ”๋žŒ์› ์ƒ์„ธ"])
# V์ž๊ณก
with river_sub[0]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ“š ์ด๋ก  ์„ ํƒ")
v_theory = st.selectbox("์นจ์‹ ๋ชจ๋ธ", list(V_VALLEY_THEORIES.keys()), key="v_th")
show_theory_card(V_VALLEY_THEORIES, v_theory)
st.markdown("---")
st.subheader("โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
st.markdown("**โฑ๏ธ ์‹œ๊ฐ„ ์Šค์ผ€์ผ**")
time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)", "์žฅ๊ธฐ (100๋งŒ~1์–ต๋…„)"],
key="v_ts", horizontal=True)
if time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
v_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 5_000, 500, key="v_t1")
elif time_scale == "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)":
v_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 100_000, 10_000, key="v_t2")
else:
v_time = st.slider("์‹œ๊ฐ„ (๋…„)", 1_000_000, 100_000_000, 10_000_000, 1_000_000, key="v_t3")
v_rock = st.slider("๐Ÿชจ ์•”์„ ๊ฒฝ๋„", 0.1, 0.9, 0.4, 0.1, key="v_r")
theory_key = V_VALLEY_THEORIES[v_theory]['key']
params = {'K': 0.0001, 'rock_hardness': v_rock}
if theory_key == "shear_stress":
params['tau_c'] = st.slider("ฯ„c (์ž„๊ณ„ ์ „๋‹จ์‘๋ ฅ)", 1.0, 20.0, 5.0, 1.0)
elif theory_key == "detachment":
params['Qs'] = st.slider("Qs (ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰๋น„)", 0.0, 0.8, 0.3, 0.1)
with c2:
result = simulate_v_valley(theory_key, v_time, params, grid_size=grid_size)
# ๊ฒฐ๊ณผ ํ‘œ์‹œ ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜
col_res, col_anim = st.columns([3, 1])
col_res.metric("V์ž๊ณก ๊นŠ์ด", f"{result['depth']:.0f} m")
col_res.metric("๊ฒฝ๊ณผ ์‹œ๊ฐ„", f"{v_time:,} ๋…„")
# Shared Plot Container
plot_container = st.empty()
# ์• ๋‹ˆ๋ฉ”์ด์…˜ ์žฌ์ƒ
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="v_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="v_anim"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {v_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘...")
anim_prog = st.progress(0)
step_size = max(1, v_time // 20)
for _ in range(n_reps):
for t in range(0, v_time + 1, step_size):
# ๋งค ํ”„๋ ˆ์ž„ ๊ณ„์‚ฐ
r_step = simulate_v_valley(theory_key, t, params, grid_size=grid_size)
# Plotly ๋ Œ๋”๋ง (๋น ๋ฆ„)
fig_step = render_terrain_plotly(r_step['elevation'],
f"V์ž๊ณก ({t:,}๋…„)",
add_water=True, water_level=r_step['elevation'].min() + 3,
texture_path="assets/reference/v_valley_texture.png", force_camera=False, water_depth_grid=r_step.get('water_depth'))
plot_container.plotly_chart(fig_step, use_container_width=True, key="v_plot_shared")
anim_prog.progress(min(1.0, t / v_time))
time.sleep(0.1)
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
# ๋งˆ์ง€๋ง‰ ์ƒํƒœ ์œ ์ง€
result = r_step
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="v_v")
if "2D" in v_mode:
fig = render_v_valley_3d(result['elevation'], result['x'],
f"V์ž๊ณก - {v_theory} ({v_time:,}๋…„)",
result['depth'])
plot_container.pyplot(fig)
plt.close()
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
plotly_fig = render_terrain_plotly(
result['elevation'],
f"V์ž๊ณก | ๊นŠ์ด: {result['depth']:.0f}m | {v_time:,}๋…„",
add_water=True, water_level=result['elevation'].min() + 3,
texture_path="assets/reference/v_valley_texture.png",
water_depth_grid=result.get('water_depth')
)
plot_container.plotly_chart(plotly_fig, use_container_width=True, key="v_plot_shared")
else:
st.image("assets/reference/v_valley_satellite_1765437288622.png",
caption="V์ž๊ณก - Google Earth ์Šคํƒ€์ผ (AI ์ƒ์„ฑ)",
use_column_width=True)
# ๊ณก๋ฅ˜
with river_sub[1]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ“š ์ด๋ก  ์„ ํƒ")
m_theory = st.selectbox("๊ณก๋ฅ˜ ๋ชจ๋ธ", list(MEANDER_THEORIES.keys()), key="m_th")
show_theory_card(MEANDER_THEORIES, m_theory)
st.markdown("---")
st.subheader("โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
st.markdown("**โฑ๏ธ ์‹œ๊ฐ„ ์Šค์ผ€์ผ**")
m_time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)", "์žฅ๊ธฐ (100๋งŒ~1์–ต๋…„)"],
key="m_ts", horizontal=True)
if m_time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
m_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 5_000, 500, key="m_t1")
elif m_time_scale == "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)":
m_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 100_000, 10_000, key="m_t2")
else:
m_time = st.slider("์‹œ๊ฐ„ (๋…„)", 1_000_000, 100_000_000, 10_000_000, 1_000_000, key="m_t3")
m_amp = st.slider("์ดˆ๊ธฐ ์ง„ํญ (m)", 10, 80, 40, 10, key="m_a")
theory_key = MEANDER_THEORIES[m_theory]['key']
params = {'init_amplitude': m_amp, 'E0': 0.4}
if theory_key == "ikeda_parker":
params['velocity'] = st.slider("U (์œ ์† m/s)", 0.5, 3.0, 1.5, 0.5)
elif theory_key == "seminara":
params['froude'] = st.slider("Fr (Froude์ˆ˜)", 0.1, 0.8, 0.3, 0.1)
with c2:
result = simulate_meander(theory_key, m_time, params)
# ๊ฒฐ๊ณผ ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜
col_res, col_anim = st.columns([3, 1])
col_res.metric("๊ตด๊ณก๋„", f"{result['sinuosity']:.2f}")
# col_res.metric("์šฐ๊ฐํ˜ธ", f"{len(result.get('oxbow_lakes', []))} ๊ฐœ")
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="m_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="m_anim"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {m_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘ (3D)...")
anim_chart = st.empty()
anim_prog = st.progress(0)
step_size = max(1, m_time // 10) # 10 frames
for _ in range(n_reps):
for t in range(0, m_time + 1, step_size):
r_step = simulate_meander(theory_key, t, params)
# 3D ๋ Œ๋”๋ง (๊ฐ€๋ณ๊ฒŒ)
fig_step = render_terrain_plotly(
r_step['elevation'],
f"์ž์œ  ๊ณก๋ฅ˜ ({t:,}๋…„)",
water_depth_grid=r_step['water_depth'],
texture_path="assets/reference/meander_texture.png"
)
anim_chart.plotly_chart(fig_step, use_container_width=True, key=f"m_anim_{t}")
anim_prog.progress(min(1.0, t / m_time))
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
result = r_step
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="m_v")
if "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
fig = render_terrain_plotly(
result['elevation'],
f"์ž์œ  ๊ณก๋ฅ˜ - {MEANDER_THEORIES[m_theory].get('description', '')[:20]}...",
water_depth_grid=result['water_depth'],
texture_path="assets/reference/meander_texture.png"
)
st.plotly_chart(fig, use_container_width=True, key="m_plot")
else:
st.image("assets/reference/meander_satellite_1765437309640.png",
caption="๊ณก๋ฅ˜ ํ•˜์ฒœ - Google Earth ์Šคํƒ€์ผ (AI ์ƒ์„ฑ)",
use_column_width=True)
# ์‚ผ๊ฐ์ฃผ
with river_sub[2]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ“š ์ด๋ก  ์„ ํƒ")
d_theory = st.selectbox("์‚ผ๊ฐ์ฃผ ๋ชจ๋ธ", list(DELTA_THEORIES.keys()), key="d_th")
show_theory_card(DELTA_THEORIES, d_theory)
st.markdown("---")
st.subheader("โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
st.markdown("**โฑ๏ธ ์‹œ๊ฐ„ ์Šค์ผ€์ผ**")
d_time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)", "์žฅ๊ธฐ (100๋งŒ~1์–ต๋…„)"],
key="d_ts", horizontal=True)
if d_time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
d_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 6_000, 500, key="d_t1")
elif d_time_scale == "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)":
d_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 200_000, 10_000, key="d_t2")
else:
d_time = st.slider("์‹œ๊ฐ„ (๋…„)", 1_000_000, 100_000_000, 20_000_000, 1_000_000, key="d_t3")
theory_key = DELTA_THEORIES[d_theory]['key']
params = {}
if theory_key == "galloway":
params['river'] = st.slider("ํ•˜์ฒœ ์—๋„ˆ์ง€", 0, 100, 55, 5) / 100
params['wave'] = st.slider("ํŒŒ๋ž‘ ์—๋„ˆ์ง€", 0, 100, 30, 5) / 100
params['tidal'] = st.slider("์กฐ๋ฅ˜ ์—๋„ˆ์ง€", 0, 100, 15, 5) / 100
elif theory_key == "orton":
params['grain'] = st.slider("์ž…์žํฌ๊ธฐ (0=์„ธ๋ฆฝ, 1=์กฐ๋ฆฝ)", 0.0, 1.0, 0.5, 0.1)
params['wave'] = st.slider("ํŒŒ๋ž‘ ์—๋„ˆ์ง€", 0, 100, 30, 5) / 100
params['tidal'] = st.slider("์กฐ๋ฅ˜ ์—๋„ˆ์ง€", 0, 100, 20, 5) / 100
elif theory_key == "bhattacharya":
params['Qsed'] = st.slider("ํ‡ด์ ๋ฌผ๋Ÿ‰ (ํ†ค/๋…„)", 10, 100, 50, 10)
params['Hs'] = st.slider("์œ ์˜ํŒŒ๊ณ  (m)", 0.5, 4.0, 1.5, 0.5)
params['Tr'] = st.slider("์กฐ์ฐจ (m)", 0.5, 6.0, 2.0, 0.5)
st.markdown("---")
params['accel'] = st.slider("โšก ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๊ฐ€์† (ํ˜„์‹ค์„ฑ vs ์†๋„)", 1.0, 20.0, 1.0, 0.5,
help="1.0์€ ๋ฌผ๋ฆฌ์ ์œผ๋กœ ์ •ํ™•ํ•œ ์†๋„์ž…๋‹ˆ๋‹ค. ๊ฐ’์„ ๋†’์ด๋ฉด ์ง€ํ˜• ๋ณ€ํ™”๊ฐ€ ๊ณผ์žฅ๋˜์–ด ๋น ๋ฅด๊ฒŒ ๋‚˜ํƒ€๋‚ฉ๋‹ˆ๋‹ค.")
with c2:
result = simulate_delta(theory_key, d_time, params, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
# ๊ฒฐ๊ณผ ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜
col_res, col_anim = st.columns([3, 1])
col_res.metric("์‚ผ๊ฐ์ฃผ ์œ ํ˜•", result['delta_type'])
col_res.metric("๋ฉด์ ", f"{result['area']:.2f} kmยฒ")
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="d_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="d_anim"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {d_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘...")
anim_prog = st.progress(0)
step_size = max(1, d_time // 20)
for _ in range(n_reps):
for t in range(0, d_time + 1, step_size):
r_step = simulate_delta(theory_key, t, params, grid_size=grid_size)
fig_step = render_terrain_plotly(r_step['elevation'],
f"{r_step['delta_type']} ({t:,}๋…„)",
add_water=True, water_level=0,
texture_path="assets/reference/delta_texture.png", force_camera=False)
plot_container.plotly_chart(fig_step, use_container_width=True, key="d_plot_shared")
anim_prog.progress(min(1.0, t / d_time))
# time.sleep(0.1)
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
anim_prog.empty()
result = r_step
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="d_v")
if "2D" in v_mode:
fig = render_terrain_3d(result['elevation'],
f"์‚ผ๊ฐ์ฃผ - {d_theory} ({d_time:,}๋…„)",
add_water=True, water_level=0,
view_elev=40, view_azim=240)
plot_container.pyplot(fig)
plt.close()
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
plotly_fig = render_terrain_plotly(
result['elevation'],
f"{result['delta_type']} | ๋ฉด์ : {result['area']:.2f} kmยฒ | {d_time:,}๋…„",
add_water=True, water_level=0,
texture_path="assets/reference/delta_texture.png",
water_depth_grid=result.get('water_depth')
)
plot_container.plotly_chart(plotly_fig, use_container_width=True, key="d_plot_shared")
else:
st.image("assets/reference/delta_satellite_1765437326499.png",
caption="์กฐ์กฑ์ƒ ์‚ผ๊ฐ์ฃผ - Google Earth ์Šคํƒ€์ผ (AI ์ƒ์„ฑ)",
use_column_width=True)
# ์„ ์ƒ์ง€
with river_sub[3]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ“ ์„ ์ƒ์ง€")
st.info("์‚ฐ์ง€์—์„œ ํ‰์ง€๋กœ ๋‚˜์˜ค๋Š” ๊ณณ์— ํ˜•์„ฑ๋˜๋Š” ๋ถ€์ฑ„๊ผด ํ‡ด์  ์ง€ํ˜•")
st.markdown("---")
af_time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)"], key="af_ts", horizontal=True)
if af_time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
af_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 5_000, 500, key="af_t1")
else:
af_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 100_000, 10_000, key="af_t2")
af_slope = st.slider("๊ฒฝ์‚ฌ", 0.1, 0.9, 0.5, 0.1, key="af_s")
af_sed = st.slider("ํ‡ด์ ๋ฌผ๋Ÿ‰", 0.1, 1.0, 0.5, 0.1, key="af_sed")
with c2:
result = simulate_alluvial_fan(af_time, {'slope': af_slope, 'sediment': af_sed}, grid_size=grid_size)
col_res, col_anim = st.columns([3, 1])
# Debug Display
if 'debug_sed_max' in result:
st.caption(f"Debug: Max Sediment = {result['debug_sed_max']:.2f}m | Steps = {result.get('debug_steps')}")
col_res.metric("์„ ์ƒ์ง€ ๋ฉด์ ", f"{result['area']:.2f} kmยฒ")
col_res.metric("์„ ์ƒ์ง€ ๋ฐ˜๊ฒฝ", f"{result['radius']:.2f} km")
# Shared Plot Container
plot_container = st.empty()
# Render using sediment grid for accurate coloring
fig = render_terrain_plotly(
result['elevation'],
"์„ ์ƒ์ง€ (Alluvial Fan)",
water_depth_grid=result.get('water_depth'),
sediment_grid=result.get('sediment'), # Pass sediment layer
force_camera=False
)
plot_container.plotly_chart(fig, use_container_width=True, key="af_plot_final")
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="af_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="af_anim"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {af_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘...")
anim_prog = st.progress(0)
step_size = max(1, af_time // 20)
for _ in range(n_reps):
for t in range(0, af_time + 1, step_size):
r_step = simulate_alluvial_fan(t, {'slope': af_slope, 'sediment': af_sed}, grid_size=grid_size)
fig_step = render_terrain_plotly(
r_step['elevation'],
f"์„ ์ƒ์ง€ ({t:,}๋…„)",
add_water=False,
force_camera=False,
water_depth_grid=r_step.get('water_depth'),
sediment_grid=r_step.get('sediment')
)
plot_container.plotly_chart(fig_step, use_container_width=True, key="af_plot_shared")
anim_prog.progress(min(1.0, t / af_time))
time.sleep(0.1)
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
anim_prog.empty()
result = r_step
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D"], horizontal=True, key="af_v")
if "2D" in v_mode:
fig = render_terrain_3d(result['elevation'], f"์„ ์ƒ์ง€ ({af_time:,}๋…„)", add_water=False)
plot_container.pyplot(fig)
plt.close()
else:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
plotly_fig = render_terrain_plotly(result['elevation'], f"์„ ์ƒ์ง€ | ๋ฉด์ : {result['area']:.2f}kmยฒ | {af_time:,}๋…„", add_water=False, texture_path="assets/reference/alluvial_fan_texture.png", water_depth_grid=result.get('water_depth'))
plot_container.plotly_chart(plotly_fig, use_container_width=True, key="af_plot_shared")
# ํ•˜์•ˆ๋‹จ๊ตฌ
with river_sub[4]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ“Š ํ•˜์•ˆ๋‹จ๊ตฌ")
st.info("ํ•˜์ฒœ ์˜†์— ๊ณ„๋‹จ ๋ชจ์–‘์œผ๋กœ ํ˜•์„ฑ๋œ ํ‰ํƒ„๋ฉด (๊ตฌ ๋ฒ”๋žŒ์›)")
st.markdown("---")
rt_time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)"], key="rt_ts", horizontal=True)
if rt_time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
rt_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 5_000, 500, key="rt_t1")
else:
rt_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 100_000, 10_000, key="rt_t2")
rt_uplift = st.slider("์ง€๋ฐ˜ ์œต๊ธฐ์œจ", 0.1, 1.0, 0.5, 0.1, key="rt_u")
rt_n = st.slider("๋‹จ๊ตฌ๋ฉด ์ˆ˜", 1, 5, 3, 1, key="rt_n")
with c2:
result = simulate_river_terrace(rt_time, {'uplift': rt_uplift, 'n_terraces': rt_n}, grid_size=grid_size)
col_res, col_anim = st.columns([3, 1])
col_res.metric("ํ˜•์„ฑ๋œ ๋‹จ๊ตฌ๋ฉด", f"{result['n_terraces']} ๋‹จ")
# Shared Plot Container
plot_container = st.empty()
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="rt_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="rt_anim"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {rt_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘...")
anim_prog = st.progress(0)
step_size = max(1, rt_time // 20)
for _ in range(n_reps):
for t in range(0, rt_time + 1, step_size):
r_step = simulate_river_terrace(t, {'uplift': rt_uplift, 'n_terraces': rt_n}, grid_size=grid_size)
fig_step = render_terrain_plotly(r_step['elevation'], f"ํ•˜์•ˆ๋‹จ๊ตฌ ({t:,}๋…„)", add_water=True, water_level=r_step['elevation'].min()+5, force_camera=False, water_depth_grid=r_step.get('water_depth'))
plot_container.plotly_chart(fig_step, use_container_width=True, key="rt_plot_shared")
anim_prog.progress(min(1.0, t / rt_time))
time.sleep(0.1)
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
anim_prog.empty()
result = r_step
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D"], horizontal=True, key="rt_v")
if "2D" in v_mode:
fig = render_terrain_3d(result['elevation'], f"ํ•˜์•ˆ๋‹จ๊ตฌ ({af_time:,}๋…„)", add_water=True, water_level=result['elevation'].min()+5)
plot_container.pyplot(fig)
plt.close()
else:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
plotly_fig = render_terrain_plotly(result['elevation'], f"ํ•˜์•ˆ๋‹จ๊ตฌ | {result['n_terraces']}๋‹จ | {rt_time:,}๋…„", add_water=True, water_level=result['elevation'].min()+5, water_depth_grid=result.get('water_depth'))
plot_container.plotly_chart(plotly_fig, use_container_width=True, key="rt_plot_shared")
# ํ•˜์ฒœ์Ÿํƒˆ
with river_sub[5]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("โš”๏ธ ํ•˜์ฒœ์Ÿํƒˆ")
st.info("์นจ์‹๋ ฅ์ด ๊ฐ•ํ•œ ํ•˜์ฒœ์ด ์ธ์ ‘ ํ•˜์ฒœ์˜ ์ƒ๋ฅ˜๋ฅผ ๋นผ์•—๋Š” ํ˜„์ƒ")
st.markdown("---")
sp_time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)"], key="sp_ts", horizontal=True)
if sp_time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
sp_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 5_000, 500, key="sp_t1")
else:
sp_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 100_000, 10_000, key="sp_t2")
sp_diff = st.slider("์นจ์‹๋ ฅ ์ฐจ์ด", 0.3, 0.9, 0.7, 0.1, key="sp_d")
with c2:
result = simulate_stream_piracy(sp_time, {'erosion_diff': sp_diff}, grid_size=grid_size)
col_res, col_anim = st.columns([3, 1])
if result['captured']:
col_res.success(f"โš”๏ธ ํ•˜์ฒœ์Ÿํƒˆ ๋ฐœ์ƒ! ({result['capture_time']:,}๋…„)")
else:
col_res.warning("์•„์ง ํ•˜์ฒœ์Ÿํƒˆ์ด ๋ฐœ์ƒํ•˜์ง€ ์•Š์Œ")
# Shared Plot Container
plot_container = st.empty()
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="sp_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="sp_anim"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {sp_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘...")
anim_prog = st.progress(0)
step_size = max(1, sp_time // 20)
for _ in range(n_reps):
for t in range(0, sp_time + 1, step_size):
r_step = simulate_stream_piracy(t, {'erosion_diff': sp_diff}, grid_size=grid_size)
status = "์Ÿํƒˆ ์ง„ํ–‰ ์ค‘"
if r_step['captured']: status = "์Ÿํƒˆ ๋ฐœ์ƒ!"
fig_step = render_terrain_plotly(r_step['elevation'], f"ํ•˜์ฒœ์Ÿํƒˆ | {status} | {t:,}๋…„", add_water=False, force_camera=False, water_depth_grid=r_step.get('water_depth'))
plot_container.plotly_chart(fig_step, use_container_width=True, key="sp_plot_shared")
anim_prog.progress(min(1.0, t / sp_time))
time.sleep(0.1)
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
anim_prog.empty()
result = r_step
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D"], horizontal=True, key="sp_v")
if "2D" in v_mode:
fig = render_terrain_3d(result['elevation'], f"ํ•˜์ฒœ์Ÿํƒˆ ({sp_time:,}๋…„)", add_water=True, water_level=result['elevation'].min()+3)
plot_container.pyplot(fig)
plt.close()
else:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
status = "์Ÿํƒˆ ์™„๋ฃŒ" if result['captured'] else "์ง„ํ–‰ ์ค‘"
plotly_fig = render_terrain_plotly(result['elevation'], f"ํ•˜์ฒœ์Ÿํƒˆ | {status} | {sp_time:,}๋…„", add_water=False, water_depth_grid=result.get('water_depth'))
plot_container.plotly_chart(plotly_fig, use_container_width=True, key="sp_plot_shared")
# ๊ฐ์ž…๊ณก๋ฅ˜
with river_sub[6]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ”„ ๊ฐ์ž…๊ณก๋ฅ˜")
st.info("์ง€๋ฐ˜ ์œต๊ธฐ๋กœ ๊ณก๋ฅ˜ ํ•˜์ฒœ์ด ๊นŠ์ด ํŒŒ๊ณ ๋“  ์ง€ํ˜•")
st.markdown("---")
em_time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)"], key="em_ts", horizontal=True)
if em_time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
em_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 5_000, 500, key="em_t1")
else:
em_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 100_000, 10_000, key="em_t2")
em_uplift = st.slider("์œต๊ธฐ์œจ", 0.1, 1.0, 0.5, 0.1, key="em_u")
em_type = st.radio("์œ ํ˜•", ["์ฐฉ๊ทผ๊ณก๋ฅ˜ (U์ž)", "๊ฐ์ž…๊ณก๋ฅ˜ (V์ž)"], key="em_type", horizontal=True)
with c2:
inc_type = 'U' if "์ฐฉ๊ทผ" in em_type else 'V'
result = simulate_entrenched_meander(em_time, {'uplift': em_uplift, 'incision_type': inc_type}, grid_size=grid_size)
col_res, col_anim = st.columns([3, 1])
col_res.metric("์œ ํ˜•", result['type'])
col_res.metric("๊นŠ์ด", f"{result['depth']:.0f} m")
# Shared Plot Container
plot_container = st.empty()
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="em_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="em_anim"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {em_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘...")
anim_prog = st.progress(0)
step_size = max(1, em_time // 20)
for _ in range(n_reps):
for t in range(0, em_time + 1, step_size):
r_step = simulate_entrenched_meander(t, {'uplift': em_uplift, 'incision_type': inc_type}, grid_size=grid_size)
fig_step = render_terrain_plotly(r_step['elevation'], f"{r_step['type']} ({t:,}๋…„)", add_water=True, water_level=r_step['elevation'].min()+5, force_camera=False, water_depth_grid=r_step.get('water_depth'))
plot_container.plotly_chart(fig_step, use_container_width=True, key="em_plot_shared")
anim_prog.progress(min(1.0, t / em_time))
time.sleep(0.1)
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
anim_prog.empty()
result = r_step
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="em_v")
if "2D" in v_mode:
fig = render_terrain_3d(result['elevation'], f"{result['type']} ({em_time:,}๋…„)", add_water=True, water_level=result['elevation'].min()+5)
plot_container.pyplot(fig)
plt.close()
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
plotly_fig = render_terrain_plotly(result['elevation'], f"{result['type']} | ๊นŠ์ด: {result['depth']:.0f}m | {em_time:,}๋…„", add_water=True, water_level=result['elevation'].min()+2, water_depth_grid=result.get('water_depth'))
plot_container.plotly_chart(plotly_fig, use_container_width=True, key="em_plot_shared")
else:
st.image("assets/reference/entrenched_meander_ref_1765496053723.png", caption="๊ฐ์ž… ๊ณก๋ฅ˜ (Entrenched Meander) - AI ์ƒ์„ฑ", use_column_width=True)
# ๋ง์ƒํ•˜์ฒœ
with river_sub[7]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐ŸŒŠ ๋ง์ƒ ํ•˜์ฒœ")
st.info("ํ‡ด์ ๋ฌผ์ด ๋งŽ๊ณ  ์œ ๋กœ๊ฐ€ ์–ฝํ˜€ ์žˆ๋Š” ํ•˜์ฒœ")
st.markdown("---")
bs_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 1000, 100, key="bs_t")
bs_sed = st.slider("ํ‡ด์ ๋ฌผ๋Ÿ‰", 0.1, 1.0, 0.8, 0.1, key="bs_sed")
bs_n = st.slider("์ˆ˜๋กœ ๊ฐœ์ˆ˜", 3, 10, 5, 1, key="bs_n")
with c2:
result = simulate_braided_stream(bs_time, {'sediment': bs_sed, 'n_channels': bs_n}, grid_size=grid_size)
# ์ค‘์ฒฉ ์ œ๊ฑฐ
cm1, col_anim = st.columns([3, 1])
cm1.metric("์œ ํ˜•", result['type'])
# Shared Plot Container
plot_container = st.empty()
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="bs_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="bs_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, bs_time+1, max(1, bs_time//20)):
r_step = simulate_braided_stream(t, {'sediment': bs_sed, 'n_channels': bs_n}, grid_size=grid_size)
fig_step = render_terrain_plotly(r_step['elevation'], f"๋ง์ƒํ•˜์ฒœ ({t}๋…„)", add_water=True, water_level=r_step['elevation'].min()+0.5, force_camera=False, water_depth_grid=r_step.get('water_depth'))
plot_container.plotly_chart(fig_step, use_container_width=True, key="bs_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="bs_v")
if "3D" in v_mode:
fig = render_terrain_plotly(result['elevation'], f"๋ง์ƒํ•˜์ฒœ ({bs_time}๋…„)", add_water=True, water_level=result['elevation'].min()+0.5, texture_path="assets/reference/braided_river_texture.png", water_depth_grid=result.get('water_depth'))
plot_container.plotly_chart(fig, use_container_width=True, key="bs_plot_shared")
else:
st.image("assets/reference/braided_river_1765410638302.png", caption="๋ง์ƒ ํ•˜์ฒœ (AI ์ƒ์„ฑ)", use_column_width=True)
# ํญํฌ
with river_sub[8]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ’ง ํญํฌ/ํฌํŠธํ™€")
st.info("๋‘๋ถ€ ์นจ์‹์œผ๋กœ ํ›„ํ‡ดํ•˜๋Š” ํญํฌ")
st.markdown("---")
wf_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 2000, 100, key="wf_t")
wf_rate = st.slider("ํ›„ํ‡ด ์†๋„", 0.1, 2.0, 0.5, 0.1, key="wf_r")
with c2:
result = simulate_waterfall(wf_time, {'retreat_rate': wf_rate}, grid_size=grid_size)
cm1, col_anim = st.columns([3, 1])
cm1.metric("์ด ํ›„ํ‡ด ๊ฑฐ๋ฆฌ", f"{result['retreat']:.1f} m")
# Shared Plot Container
plot_container = st.empty()
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="wf_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="wf_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, wf_time+1, max(1, wf_time//20)):
r_step = simulate_waterfall(t, {'retreat_rate': wf_rate}, grid_size=grid_size)
fig_step = render_terrain_plotly(r_step['elevation'], f"ํญํฌ ({t}๋…„)", add_water=True, water_level=90, force_camera=False, water_depth_grid=r_step.get('water_depth'))
plot_container.plotly_chart(fig_step, use_container_width=True, key="wf_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="wf_v")
if "3D" in v_mode:
fig = render_terrain_plotly(result['elevation'], f"ํญํฌ ({wf_time}๋…„)", add_water=True, water_level=90, water_depth_grid=result.get('water_depth'))
plot_container.plotly_chart(fig, use_container_width=True, key="wf_plot_shared")
else:
st.image("assets/reference/waterfall_gorge_formation_1765410495876.png", caption="ํญํฌ ๋ฐ ํ˜‘๊ณก (AI ์ƒ์„ฑ)", use_column_width=True)
# ๋ฒ”๋žŒ์› ์ƒ์„ธ
with river_sub[9]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐ŸŒพ ์ž์—ฐ์ œ๋ฐฉ/๋ฐฐํ›„์Šต์ง€")
st.info("ํ™์ˆ˜ ์‹œ ํ‡ด์  ์ฐจ์ด๋กœ ํ˜•์„ฑ๋˜๋Š” ๋ฏธ์ง€ํ˜•")
st.markdown("---")
lv_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 5000, 1000, 100, key="lv_t")
lv_freq = st.slider("๋ฒ”๋žŒ ๋นˆ๋„", 0.1, 1.0, 0.5, 0.1, key="lv_f")
with c2:
result = simulate_levee(lv_time, {'flood_freq': lv_freq}, grid_size=grid_size)
cm1, col_anim = st.columns([3, 1])
cm1.metric("์ œ๋ฐฉ ๋†’์ด", f"{result['levee_height']:.1f} m")
# Shared Plot Container
plot_container = st.empty()
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="lv_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="lv_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, lv_time+1, max(1, lv_time//20)):
r_step = simulate_levee(t, {'flood_freq': lv_freq}, grid_size=grid_size)
fig_step = render_terrain_plotly(r_step['elevation'], f"๋ฒ”๋žŒ์› ({t}๋…„)", add_water=True, water_level=42, force_camera=False, water_depth_grid=r_step.get('water_depth'))
plot_container.plotly_chart(fig_step, use_container_width=True, key="lv_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="lv_v")
if "3D" in v_mode:
fig = render_terrain_plotly(result['elevation'], f"๋ฒ”๋žŒ์› ์ƒ์„ธ ({lv_time}๋…„)", add_water=True, water_level=42, water_depth_grid=result.get('water_depth'))
plot_container.plotly_chart(fig, use_container_width=True, key="lv_plot_shared")
else:
st.image("assets/reference/floodplain_landforms_1765436731483.png", caption="๋ฒ”๋žŒ์› - ์ž์—ฐ์ œ๋ฐฉ๊ณผ ๋ฐฐํ›„์Šต์ง€ (AI ์ƒ์„ฑ)", use_column_width=True)
# ===== ํ•ด์•ˆ ์ง€ํ˜• =====
with tab_coast:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ“š ์ด๋ก  ์„ ํƒ")
co_theory = st.selectbox("ํ•ด์•ˆ ์นจ์‹ ๋ชจ๋ธ", list(COASTAL_THEORIES.keys()), key="co_th")
show_theory_card(COASTAL_THEORIES, co_theory)
st.markdown("---")
st.subheader("โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
# 3๋‹จ๊ณ„ ์‹œ๊ฐ„ ์Šค์ผ€์ผ
st.markdown("**โฑ๏ธ ์‹œ๊ฐ„ ์Šค์ผ€์ผ**")
co_time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)", "์žฅ๊ธฐ (100๋งŒ~1์–ต๋…„)"],
key="co_ts", horizontal=True)
if co_time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
co_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 3_000, 500, key="co_t1")
elif co_time_scale == "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)":
co_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 50_000, 10_000, key="co_t2")
else:
co_time = st.slider("์‹œ๊ฐ„ (๋…„)", 1_000_000, 100_000_000, 5_000_000, 1_000_000, key="co_t3")
co_wave = st.slider("๐ŸŒŠ ํŒŒ๊ณ  (m)", 0.5, 5.0, 2.0, 0.5, key="co_w")
co_rock = st.slider("๐Ÿชจ ์•”์„ ์ €ํ•ญ", 0.1, 0.9, 0.5, 0.1, key="co_r")
theory_key = COASTAL_THEORIES[co_theory]['key']
params = {'wave_height': co_wave, 'rock_resistance': co_rock}
if theory_key == "cliff_retreat":
params['Hc'] = st.slider("Hc (์ž„๊ณ„ํŒŒ๊ณ )", 0.5, 3.0, 1.5, 0.5)
elif theory_key == "cerc":
params['theta'] = st.slider("ฮธ (ํŒŒํ–ฅ๊ฐ)", 0, 45, 15, 5)
elif theory_key == "spit":
params['drift_strength'] = st.slider("์—ฐ์•ˆ๋ฅ˜ ๊ฐ•๋„", 0.1, 1.0, 0.5, 0.1)
params['sand_supply'] = st.slider("๋ชจ๋ž˜ ๊ณต๊ธ‰๋Ÿ‰", 0.1, 1.0, 0.5, 0.1)
params['wave_angle'] = st.slider("ํŒŒ๋ž‘ ๊ฐ๋„", 0, 90, 45, 5)
elif theory_key == "tombolo":
params['island_dist'] = st.slider("์„ฌ ๊ฑฐ๋ฆฌ", 0.1, 1.0, 0.5, 0.1)
params['island_size'] = st.slider("์„ฌ ํฌ๊ธฐ", 0.1, 1.0, 0.5, 0.1)
params['wave_energy'] = st.slider("ํŒŒ๋ž‘ ์—๋„ˆ์ง€", 0.1, 1.0, 0.5, 0.1)
elif theory_key == "tidal_flat":
params['tidal_range'] = st.slider("์กฐ์ฐจ(m)", 0.5, 8.0, 4.0, 0.5)
elif theory_key == "spit":
params['drift_strength'] = st.slider("์—ฐ์•ˆ๋ฅ˜ ๊ฐ•๋„", 0.1, 1.0, 0.5, 0.1)
params['sand_supply'] = st.slider("๋ชจ๋ž˜ ๊ณต๊ธ‰๋Ÿ‰", 0.1, 1.0, 0.5, 0.1)
params['wave_angle'] = st.slider("ํŒŒ๋ž‘ ๊ฐ๋„", 0, 90, 45, 5)
elif theory_key == "tombolo":
params['island_dist'] = st.slider("์„ฌ ๊ฑฐ๋ฆฌ", 0.1, 1.0, 0.5, 0.1)
params['island_size'] = st.slider("์„ฌ ํฌ๊ธฐ", 0.1, 1.0, 0.5, 0.1)
params['wave_energy'] = st.slider("ํŒŒ๋ž‘ ์—๋„ˆ์ง€", 0.1, 1.0, 0.5, 0.1)
elif theory_key == "tidal_flat":
params['tidal_range'] = st.slider("์กฐ์ฐจ(m)", 0.5, 8.0, 4.0, 0.5)
with c2:
if theory_key in ["spit", "tombolo", "tidal_flat"]:
result = simulate_coastal_deposition(theory_key, co_time, params, grid_size=grid_size)
# ํ‡ด์  ์ง€ํ˜• ๊ฒฐ๊ณผ (๋ฉ”ํŠธ๋ฆญ ์—†์Œ, ์œ ํ˜•๋งŒ ํ‘œ์‹œ)
st.info(f"์ง€ํ˜• ์œ ํ˜•: {result['type']}")
# Shared Plot Container
plot_container = st.empty()
# ์• ๋‹ˆ๋ฉ”์ด์…˜
_, col_anim = st.columns([3, 1])
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="co_loop_dep")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="co_anim_dep"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {co_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘...")
anim_prog = st.progress(0)
step_size = max(1, co_time // 20)
for _ in range(n_reps):
for t in range(0, co_time + 1, step_size):
r_step = simulate_coastal_deposition(theory_key, t, params, grid_size=grid_size)
fig_step = render_terrain_plotly(r_step['elevation'],
f"{r_step['type']} ({t:,}๋…„)",
add_water=True, water_level=0, force_camera=False)
plot_container.plotly_chart(fig_step, use_container_width=True, key="co_dep_plot_shared")
anim_prog.progress(min(1.0, t / co_time))
time.sleep(0.1)
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
anim_prog.empty()
result = r_step
else:
result = simulate_coastal(theory_key, co_time, params, grid_size=grid_size)
# Shared Plot Container (Erosion)
plot_container = st.empty()
# ๊ฒฐ๊ณผ ๋ฐ ์• ๋‹ˆ๋ฉ”์ด์…˜
# ์นจ์‹ ์ง€ํ˜• ์ „์šฉ ๋ฉ”ํŠธ๋ฆญ
cm1, cm2, cm3, col_anim = st.columns([1, 1, 1, 1])
cm1.metric("ํ•ด์‹์•  ํ›„ํ‡ด", f"{result['cliff_retreat']:.1f} m")
cm2.metric("ํŒŒ์‹๋Œ€ ํญ", f"{result['platform_width']:.1f} m")
cm3.metric("๋…ธ์น˜ ๊นŠ์ด", f"{result['notch_depth']:.1f} m")
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="co_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="co_anim"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {co_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘...")
anim_prog = st.progress(0)
step_size = max(1, co_time // 20)
for _ in range(n_reps):
for t in range(0, co_time + 1, step_size):
r_step = simulate_coastal(theory_key, t, params, grid_size=grid_size)
fig_step = render_terrain_plotly(r_step['elevation'],
f"ํ•ด์•ˆ์นจ์‹ ({t:,}๋…„)",
add_water=True, water_level=0, force_camera=False)
plot_container.plotly_chart(fig_step, use_container_width=True, key="co_plot_shared")
anim_prog.progress(min(1.0, t / co_time))
time.sleep(0.1)
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
anim_prog.empty()
result = r_step
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="co_v")
if "2D" in v_mode:
fig = render_terrain_3d(result['elevation'],
f"ํ•ด์•ˆ ์ง€ํ˜• - {co_theory} ({co_time:,}๋…„)",
add_water=True, water_level=0,
view_elev=35, view_azim=210)
plot_container.pyplot(fig)
plt.close()
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
plotly_fig = render_terrain_plotly(
result['elevation'],
f"ํ•ด์•ˆ์นจ์‹ | ํ›„ํ‡ด: {result['cliff_retreat']:.1f}m | {co_time:,}๋…„",
add_water=True, water_level=0
)
plot_container.plotly_chart(plotly_fig, use_container_width=True, key="co_plot_shared")
else:
if theory_key == "cliff_retreat":
st.image("assets/reference/sea_stack_arch_ref_1765495979396.png", caption="์‹œ์Šคํƒ & ํ•ด์‹์•„์น˜ - AI ์ƒ์„ฑ", use_column_width=True)
elif theory_key in ["tombolo", "spit"]:
st.image("assets/reference/tombolo_sandbar_ref_1765495999194.png", caption="์œก๊ณ„๋„ & ์‚ฌ์ทจ - AI ์ƒ์„ฑ", use_column_width=True)
else:
st.info("์ด ์ง€ํ˜•์— ๋Œ€ํ•œ ์ฐธ๊ณ  ์‚ฌ์ง„์ด ์•„์ง ์—†์Šต๋‹ˆ๋‹ค.")
# ===== ์นด๋ฅด์ŠคํŠธ =====
# ===== ์นด๋ฅด์ŠคํŠธ =====
with tab_karst:
ka_subs = st.tabs(["๐Ÿœ๏ธ ๋Œ๋ฆฌ๋„ค (Doline)", "โ›ฐ๏ธ ํƒ‘ ์นด๋ฅด์ŠคํŠธ (Tower)", "๐Ÿฆ‡ ์„ํšŒ๋™๊ตด (Cave)"])
# ๋Œ๋ฆฌ๋„ค
with ka_subs[0]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿœ๏ธ ๋Œ๋ฆฌ๋„ค (Doline)")
ka_theory = st.selectbox("์šฉ์‹ ๋ชจ๋ธ", list(KARST_THEORIES.keys()), key="ka_th")
show_theory_card(KARST_THEORIES, ka_theory)
st.markdown("---")
ka_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 100_000, 10_000, 500, key="ka_t")
ka_co2 = st.slider("COโ‚‚ ๋†๋„", 0.1, 1.0, 0.5, 0.1, key="ka_co2")
ka_rain = st.slider("๊ฐ•์ˆ˜๋Ÿ‰", 0.1, 1.0, 0.5, 0.1, key="ka_rain")
with c2:
params = {'co2': ka_co2, 'rainfall': ka_rain}
result = simulate_karst(KARST_THEORIES[ka_theory]['key'], ka_time, params, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
_, col_anim = st.columns([3, 1])
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="ka_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="ka_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, ka_time+1, max(1, ka_time//20)):
r = simulate_karst(KARST_THEORIES[ka_theory]['key'], t, params, grid_size=grid_size)
f = render_terrain_plotly(r['elevation'], f"์นด๋ฅด์ŠคํŠธ ({t:,}๋…„)", add_water=False, force_camera=False)
plot_container.plotly_chart(f, use_container_width=True, key="ka_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="ka_v")
if "2D" in v_mode:
f = render_terrain_plotly(result['elevation'], f"์นด๋ฅด์ŠคํŠธ ({ka_time:,}๋…„)", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="ka_plot_shared")
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
f = render_terrain_plotly(result['elevation'], f"๋Œ๋ฆฌ๋„ค | {ka_time:,}๋…„", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="ka_plot_shared")
else:
st.image("assets/reference/doline_sinkhole_1765436375545.png", caption="๋Œ๋ฆฌ๋„ค (AI ์ƒ์„ฑ)", use_column_width=True)
# ํƒ‘ ์นด๋ฅด์ŠคํŠธ
with ka_subs[1]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("โ›ฐ๏ธ ํƒ‘ ์นด๋ฅด์ŠคํŠธ (Tower)")
st.info("์ฐจ๋ณ„ ์šฉ์‹์œผ๋กœ ํ‰์•ผ ์œ„์— ๋‚จ์€ ์„ํšŒ์•” ๋ด‰์šฐ๋ฆฌ๋“ค")
st.markdown("---")
tk_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 500_000, 100_000, 10_000, key="tk_t")
tk_rate = st.slider("์šฉ์‹๋ฅ ", 0.1, 1.0, 0.5, 0.1, key="tk_r")
with c2:
result = simulate_tower_karst(tk_time, {'erosion_rate': tk_rate}, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
_, col_anim = st.columns([3, 1])
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="tk_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="tk_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, tk_time+1, max(1, tk_time//20)):
r = simulate_tower_karst(t, {'erosion_rate': tk_rate}, grid_size=grid_size)
f = render_terrain_plotly(r['elevation'], f"ํƒ‘ ์นด๋ฅด์ŠคํŠธ ({t:,}๋…„)", add_water=False, force_camera=False)
plot_container.plotly_chart(f, use_container_width=True, key="tk_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ AI ์œ„์„ฑ์‚ฌ์ง„"], horizontal=True, key="tk_v")
if "2D" in v_mode:
f = render_terrain_plotly(result['elevation'], f"ํƒ‘ ์นด๋ฅด์ŠคํŠธ ({tk_time:,}๋…„)", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="tk_plot_shared")
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
f = render_terrain_plotly(result['elevation'], f"ํƒ‘ ์นด๋ฅด์ŠคํŠธ | {tk_time:,}๋…„", add_water=False, texture_path="assets/reference/tower_karst_texture.png")
plot_container.plotly_chart(f, use_container_width=True, key="tk_plot_shared")
else:
st.image("assets/reference/tower_karst_ref.png", caption="ํƒ‘ ์นด๋ฅด์ŠคํŠธ (Guilin) - AI ์ƒ์„ฑ", use_column_width=True)
# ์„ํšŒ๋™๊ตด
with ka_subs[2]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿฆ‡ ์„ํšŒ๋™๊ตด (Cave)")
st.info("์ง€ํ•˜์ˆ˜์˜ ์šฉ์‹๊ณผ ์นจ์ „์œผ๋กœ ํ˜•์„ฑ๋œ ๋™๊ตด๊ณผ ์ƒ์„ฑ๋ฌผ (์„์ˆœ)")
st.markdown("---")
cv_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 500_000, 50_000, 5000, key="cv_t")
cv_rate = st.slider("์„ฑ์žฅ ์†๋„", 0.1, 1.0, 0.5, 0.1, key="cv_r")
with c2:
result = simulate_cave(cv_time, {'rate': cv_rate}, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
_, col_anim = st.columns([3, 1])
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="cv_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="cv_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, cv_time+1, max(1, cv_time//20)):
r = simulate_cave(t, {'rate': cv_rate}, grid_size=grid_size)
f = render_terrain_plotly(r['elevation'], f"์„ํšŒ๋™๊ตด ({t:,}๋…„)", add_water=False, force_camera=False)
plot_container.plotly_chart(f, use_container_width=True, key="cv_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="cv_v")
if "2D" in v_mode:
f = render_terrain_plotly(result['elevation'], f"์„ํšŒ๋™๊ตด ๋ฐ”๋‹ฅ ({cv_time:,}๋…„)", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="cv_plot_shared")
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
f = render_terrain_plotly(result['elevation'], f"์„ํšŒ๋™๊ตด | {cv_time:,}๋…„", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="cv_plot_shared")
else:
st.image("assets/reference/cave_ref.png", caption="์„ํšŒ๋™๊ตด ๋‚ด๋ถ€ - AI ์ƒ์„ฑ", use_column_width=True)
# ===== ํ™”์‚ฐ =====
with tab_volcano:
vo_subs = st.tabs(["๐ŸŒ‹ ํ™”์‚ฐ์ฒด/์นผ๋ฐ๋ผ", "๐Ÿœ๏ธ ์šฉ์•” ๋Œ€์ง€", "๐Ÿ›๏ธ ์ฃผ์ƒ์ ˆ๋ฆฌ"])
# ๊ธฐ๋ณธ ํ™”์‚ฐ
with vo_subs[0]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("ํ™”์‚ฐ์ฒด/์นผ๋ฐ๋ผ")
vo_theory = st.selectbox("ํ™”์‚ฐ ์œ ํ˜•", list(VOLCANIC_THEORIES.keys()), key="vo_th")
show_theory_card(VOLCANIC_THEORIES, vo_theory)
st.markdown("---")
vo_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 2_000_000, 500_000, 10_000, key="vo_t")
vo_rate = st.slider("๋ถ„์ถœ๋ฅ ", 0.1, 1.0, 0.5, 0.1, key="vo_rate")
params = {'eruption_rate': vo_rate}
if VOLCANIC_THEORIES[vo_theory]['key'] == "shield":
params['viscosity'] = st.slider("์šฉ์•” ์ ์„ฑ", 0.1, 0.5, 0.3, 0.1)
elif VOLCANIC_THEORIES[vo_theory]['key'] == "caldera":
params['caldera_size'] = st.slider("์นผ๋ฐ๋ผ ํฌ๊ธฐ", 0.3, 1.0, 0.5, 0.1)
with c2:
result = simulate_volcanic(VOLCANIC_THEORIES[vo_theory]['key'], vo_time, params, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
_, col_anim = st.columns([3, 1])
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="vo_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, vo_time+1, max(1, vo_time//20)):
r = simulate_volcanic(VOLCANIC_THEORIES[vo_theory]['key'], t, params, grid_size=grid_size)
f = render_terrain_plotly(r['elevation'], f"{r['type']} ({t:,}๋…„)", add_water=False, texture_path="assets/reference/volcano_texture.png", force_camera=False)
plot_container.plotly_chart(f, use_container_width=True, key="vo_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="vo_v")
if "3D" in v_mode:
f = render_terrain_plotly(result['elevation'], f"{result['type']} ({vo_time:,}๋…„)", add_water=False, texture_path="assets/reference/volcano_texture.png")
plot_container.plotly_chart(f, use_container_width=True, key="vo_plot_shared")
else:
# ํ™”์‚ฐ ์œ ํ˜•์— ๋”ฐ๋ผ ๋‹ค๋ฅธ ์ด๋ฏธ์ง€
if "shield" in VOLCANIC_THEORIES[vo_theory]['key']:
st.image("assets/reference/shield_vs_stratovolcano_1765436448576.png", caption="์ˆœ์ƒ ํ™”์‚ฐ (AI ์ƒ์„ฑ)", use_column_width=True)
else:
st.image("assets/reference/caldera_formation_1765436466778.png", caption="์นผ๋ฐ๋ผ (AI ์ƒ์„ฑ)", use_column_width=True)
# ์šฉ์•” ๋Œ€์ง€
with vo_subs[1]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿœ๏ธ ์šฉ์•” ๋Œ€์ง€")
st.info("์œ ๋™์„ฑ์ด ํฐ ํ˜„๋ฌด์•”์งˆ ์šฉ์•”์ด ์—ดํ•˜ ๋ถ„์ถœํ•˜์—ฌ ํ˜•์„ฑ๋œ ๋Œ€์ง€")
st.markdown("---")
lp_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 1_000_000, 100_000, 10_000, key="lp_t")
lp_rate = st.slider("๋ถ„์ถœ๋ฅ ", 0.1, 1.0, 0.8, 0.1, key="lp_r")
with c2:
result = simulate_lava_plateau(lp_time, {'eruption_rate': lp_rate}, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
_, col_anim = st.columns([3, 1])
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="lp_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="lp_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, lp_time+1, max(1, lp_time//20)):
r = simulate_lava_plateau(t, {'eruption_rate': lp_rate}, grid_size=grid_size)
f = render_terrain_plotly(r['elevation'], f"์šฉ์•”๋Œ€์ง€ ({t:,}๋…„)", add_water=False, force_camera=False)
plot_container.plotly_chart(f, use_container_width=True, key="lp_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ AI ์œ„์„ฑ์‚ฌ์ง„"], horizontal=True, key="lp_v")
if "2D" in v_mode:
f = render_terrain_plotly(result['elevation'], f"์šฉ์•”๋Œ€์ง€ ({lp_time:,}๋…„)", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="lp_plot_shared")
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
f = render_terrain_plotly(result['elevation'], f"์šฉ์•”๋Œ€์ง€ | {lp_time:,}๋…„", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="lp_plot_shared")
else:
st.image("assets/reference/lava_plateau_ref.png", caption="์šฉ์•”๋Œ€์ง€ (Iceland) - AI ์ƒ์„ฑ", use_column_width=True)
# ์ฃผ์ƒ์ ˆ๋ฆฌ
with vo_subs[2]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ›๏ธ ์ฃผ์ƒ์ ˆ๋ฆฌ")
st.info("์šฉ์•”์˜ ๋ƒ‰๊ฐ ๋ฐ ์ˆ˜์ถ•์œผ๋กœ ํ˜•์„ฑ๋œ ์œก๊ฐํ˜• ๊ธฐ๋‘ฅ ํŒจํ„ด")
st.markdown("---")
cj_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 50_000, 5000, 100, key="cj_t")
cj_rate = st.slider("์นจ์‹(ํ’ํ™”)๋ฅ ", 0.1, 1.0, 0.5, 0.1, key="cj_r")
with c2:
result = simulate_columnar_jointing(cj_time, {'erosion_rate': cj_rate}, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
_, col_anim = st.columns([3, 1])
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="cj_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="cj_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, cj_time+1, max(1, cj_time//20)):
r = simulate_columnar_jointing(t, {'erosion_rate': cj_rate}, grid_size=grid_size)
f = render_terrain_plotly(r['elevation'], f"์ฃผ์ƒ์ ˆ๋ฆฌ ({t:,}๋…„)", add_water=True, water_level=80, force_camera=False)
plot_container.plotly_chart(f, use_container_width=True, key="cj_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="cj_v")
if "2D" in v_mode:
f = render_terrain_plotly(result['elevation'], f"์ฃผ์ƒ์ ˆ๋ฆฌ ({cj_time:,}๋…„)", add_water=True, water_level=80)
plot_container.plotly_chart(f, use_container_width=True, key="cj_plot_shared")
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
f = render_terrain_plotly(result['elevation'], f"์ฃผ์ƒ์ ˆ๋ฆฌ | {cj_time:,}๋…„", add_water=True, water_level=80)
plot_container.plotly_chart(f, use_container_width=True, key="cj_plot_shared")
else:
st.image("assets/reference/columnar_ref.png", caption="์ฃผ์ƒ์ ˆ๋ฆฌ (Basalt Columns) - AI ์ƒ์„ฑ", use_column_width=True)
# ===== ๋น™ํ•˜ =====
with tab_glacial:
gl_subs = st.tabs(["๐Ÿ”๏ธ U์ž๊ณก/ํ”ผ์˜ค๋ฅด", "๐Ÿฅฃ ๊ถŒ๊ณก (Cirque)", "๐Ÿ›ค๏ธ ๋ชจ๋ ˆ์ธ (Moraine)"])
# U์ž๊ณก (๊ธฐ์กด)
with gl_subs[0]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("U์ž๊ณก/ํ”ผ์˜ค๋ฅด")
gl_type = st.radio("์œ ํ˜•", ["๋น™์‹๊ณก (U์ž๊ณก)", "ํ”ผ์˜ค๋ฅด (Fjord)"], key="gl_t_sel")
gl_theory = gl_type
st.markdown("---")
gl_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 1_000_000, 500_000, 10_000, key="gl_t")
gl_ice = st.slider("๋น™ํ•˜ ๋‘๊ป˜", 0.1, 1.0, 0.5, 0.1, key="gl_ice")
with c2:
key = "fjord" if "ํ”ผ์˜ค๋ฅด" in gl_type else "erosion"
result = simulate_glacial(key, gl_time, {'ice_thickness': gl_ice}, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
_, col_anim = st.columns([3, 1])
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="gl_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="gl_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, gl_time+1, max(1, gl_time//20)):
r = simulate_glacial(key, t, {'ice_thickness': gl_ice}, grid_size=grid_size)
tex_path = "assets/reference/fjord_texture.png" if key == "fjord" else None
f = render_terrain_plotly(r['elevation'], f"{gl_type} ({t:,}๋…„)", add_water=(key=="fjord"), water_level=100 if key=="fjord" else 0, texture_path=tex_path, force_camera=False)
plot_container.plotly_chart(f, use_container_width=True, key="gl_plot_shared")
time.sleep(0.1)
tex_path = "assets/reference/fjord_texture.png" if key == "fjord" else None
f = render_terrain_plotly(result['elevation'], f"{gl_type} ({gl_time:,}๋…„)", add_water=(key=="fjord"), water_level=100 if key=="fjord" else 0, texture_path=tex_path)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="gl_v")
if "3D" in v_mode:
plot_container.plotly_chart(f, use_container_width=True, key="gl_plot_shared")
else:
st.image("assets/reference/fjord_valley_ref_1765495963491.png", caption="ํ”ผ์˜ค๋ฅด (Fjord) - AI ์ƒ์„ฑ", use_column_width=True)
# ๊ถŒ๊ณก
with gl_subs[1]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿฅฃ ๊ถŒ๊ณก (Cirque)")
st.info("๋น™ํ•˜์˜ ํšŒ์ „ ์Šฌ๋ผ์ด๋”ฉ์œผ๋กœ ํ˜•์„ฑ๋œ ๋ฐ˜์›ํ˜• ์™€์ง€")
st.markdown("---")
cq_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 500_000, 100_000, 10_000, key="cq_t")
cq_rate = st.slider("์นจ์‹๋ฅ ", 0.1, 1.0, 0.5, 0.1, key="cq_r")
with c2:
result = simulate_cirque(cq_time, {'erosion_rate': cq_rate}, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
_, col_anim = st.columns([3, 1])
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="cq_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="cq_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, cq_time+1, max(1, cq_time//20)):
r = simulate_cirque(t, {'erosion_rate': cq_rate}, grid_size=grid_size)
f = render_terrain_plotly(r['elevation'], f"๊ถŒ๊ณก ({t:,}๋…„)", add_water=False, force_camera=False)
plot_container.plotly_chart(f, use_container_width=True, key="cq_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ AI ์œ„์„ฑ์‚ฌ์ง„"], horizontal=True, key="cq_v")
if "2D" in v_mode:
f = render_terrain_plotly(result['elevation'], f"๊ถŒ๊ณก ({cq_time:,}๋…„)", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="cq_plot_shared")
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
f = render_terrain_plotly(result['elevation'], f"๊ถŒ๊ณก | {cq_time:,}๋…„", add_water=False, texture_path="assets/reference/cirque_texture.png")
plot_container.plotly_chart(f, use_container_width=True, key="cq_plot_shared")
else:
st.image("assets/reference/cirque_ref.png", caption="๊ถŒ๊ณก (Glacial Cirque) - AI ์ƒ์„ฑ", use_column_width=True)
# ๋ชจ๋ ˆ์ธ
with gl_subs[2]:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ›ค๏ธ ๋ชจ๋ ˆ์ธ (Moraine)")
st.info("๋น™ํ•˜๊ฐ€ ์šด๋ฐ˜ํ•œ ํ‡ด์ ๋ฌผ์ด ์Œ“์ธ ์ œ๋ฐฉ")
st.markdown("---")
mo_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 100_000, 20_000, 1000, key="mo_t")
mo_sup = st.slider("ํ‡ด์ ๋ฌผ ๊ณต๊ธ‰", 0.1, 1.0, 0.5, 0.1, key="mo_s")
with c2:
result = simulate_moraine(mo_time, {'debris_supply': mo_sup}, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
_, col_anim = st.columns([3, 1])
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="mo_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="mo_anim"):
n_reps = 3 if do_loop else 1
for _ in range(n_reps):
for t in range(0, mo_time+1, max(1, mo_time//20)):
r = simulate_moraine(t, {'debris_supply': mo_sup}, grid_size=grid_size)
f = render_terrain_plotly(r['elevation'], f"๋ชจ๋ ˆ์ธ ({t:,}๋…„)", add_water=False, force_camera=False)
plot_container.plotly_chart(f, use_container_width=True, key="mo_plot_shared")
time.sleep(0.1)
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ AI ์œ„์„ฑ์‚ฌ์ง„"], horizontal=True, key="mo_v")
if "2D" in v_mode:
f = render_terrain_plotly(result['elevation'], f"๋ชจ๋ ˆ์ธ ({mo_time:,}๋…„)", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="mo_plot_shared")
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „/์คŒ**")
f = render_terrain_plotly(result['elevation'], f"๋ชจ๋ ˆ์ธ | {mo_time:,}๋…„", add_water=False)
plot_container.plotly_chart(f, use_container_width=True, key="mo_plot_shared")
else:
st.image("assets/reference/moraine_ref.png", caption="๋ชจ๋ ˆ์ธ (Moraine) - AI ์ƒ์„ฑ", use_column_width=True)
# ===== ๊ฑด์กฐ =====
with tab_arid:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ“š ์ด๋ก  ์„ ํƒ")
ar_theory = st.selectbox("๊ฑด์กฐ ์ง€ํ˜•", list(ARID_THEORIES.keys()), key="ar_th")
show_theory_card(ARID_THEORIES, ar_theory)
st.markdown("---")
st.subheader("โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
ar_time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)", "์žฅ๊ธฐ (100๋งŒ~1์–ต๋…„)"], key="ar_ts", horizontal=True)
if ar_time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
ar_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 3_000, 500, key="ar_t1")
elif ar_time_scale == "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)":
ar_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 50_000, 10_000, key="ar_t2")
else:
ar_time = st.slider("์‹œ๊ฐ„ (๋…„)", 1_000_000, 100_000_000, 5_000_000, 1_000_000, key="ar_t3")
ar_wind = st.slider("ํ’์†", 0.1, 1.0, 0.5, 0.1, key="ar_wind")
params = {'wind_speed': ar_wind}
if ARID_THEORIES[ar_theory]['key'] == "mesa":
params['rock_hardness'] = st.slider("์•”์„ ๊ฒฝ๋„", 0.1, 0.9, 0.5, 0.1)
with c2:
result = simulate_arid(ARID_THEORIES[ar_theory]['key'], ar_time, params, grid_size=grid_size)
col_res, col_anim = st.columns([3, 1])
col_res.metric("์ง€ํ˜• ์œ ํ˜•", result['type'])
# Shared Plot Container
plot_container = st.empty()
do_loop = col_anim.checkbox("๐Ÿ” ๋ฐ˜๋ณต", key="ar_loop")
if col_anim.button("โ–ถ๏ธ ์žฌ์ƒ", key="ar_anim"):
n_reps = 3 if do_loop else 1
st.info(f"โณ {ar_time:,}๋…„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์žฌ์ƒ ์ค‘...")
anim_prog = st.progress(0)
step_size = max(1, ar_time // 20)
for _ in range(n_reps):
for t in range(0, ar_time + 1, step_size):
r_step = simulate_arid(ARID_THEORIES[ar_theory]['key'], t, params, grid_size=grid_size)
fig_step = render_terrain_plotly(r_step['elevation'],
f"{r_step['type']} ({t:,}๋…„)",
add_water=False, force_camera=False)
plot_container.plotly_chart(fig_step, use_container_width=True, key="ar_plot_shared")
anim_prog.progress(min(1.0, t / ar_time))
time.sleep(0.1)
st.success("์žฌ์ƒ ์™„๋ฃŒ!")
anim_prog.empty()
result = r_step
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="ar_v")
if "2D" in v_mode:
fig = render_terrain_3d(result['elevation'], f"๊ฑด์กฐ - {ar_theory} ({ar_time:,}๋…„)", add_water=False)
plot_container.pyplot(fig)
plt.close()
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
# ๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ์ธ ๊ฒฝ์šฐ ํ…์Šค์ฒ˜ ์ ์šฉ
tex_path = None
if ARID_THEORIES[ar_theory]['key'] == "barchan":
tex_path = "assets/reference/barchan_dune_texture_topdown_1765496401371.png"
plotly_fig = render_terrain_plotly(result['elevation'],
f"{result['type']} | {ar_time:,}๋…„",
add_water=False,
texture_path=tex_path)
plot_container.plotly_chart(plotly_fig, use_container_width=True, key="ar_plot_shared")
else:
# ์ด๋ก  ํ‚ค์— ๋”ฐ๋ผ ์ด๋ฏธ์ง€ ๋ถ„๊ธฐ
tk = ARID_THEORIES[ar_theory]['key']
if tk == "barchan":
st.image("assets/reference/barchan_dune_ref_1765496023768.png", caption="๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ - AI ์ƒ์„ฑ", use_column_width=True)
elif tk == "mesa":
st.image("assets/reference/mesa_butte_ref_1765496038880.png", caption="๋ฉ”์‚ฌ & ๋ทฐํŠธ - AI ์ƒ์„ฑ", use_column_width=True)
else:
st.info("์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.")
# ===== ํ‰์•ผ =====
with tab_plain:
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("๐Ÿ“š ์ด๋ก  ์„ ํƒ")
pl_theory = st.selectbox("ํ‰์•ผ ๋ชจ๋ธ", list(PLAIN_THEORIES.keys()), key="pl_th")
show_theory_card(PLAIN_THEORIES, pl_theory)
st.markdown("---")
st.subheader("โš™๏ธ ํŒŒ๋ผ๋ฏธํ„ฐ")
pl_time_scale = st.radio("์‹œ๊ฐ„ ๋ฒ”์œ„", ["์ดˆ๊ธฐ (0~๋งŒ๋…„)", "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)", "์žฅ๊ธฐ (100๋งŒ~1์–ต๋…„)"], key="pl_ts", horizontal=True)
if pl_time_scale == "์ดˆ๊ธฐ (0~๋งŒ๋…„)":
pl_time = st.slider("์‹œ๊ฐ„ (๋…„)", 0, 10_000, 5_000, 500, key="pl_t1")
elif pl_time_scale == "์ค‘๊ธฐ (1๋งŒ~100๋งŒ๋…„)":
pl_time = st.slider("์‹œ๊ฐ„ (๋…„)", 10_000, 1_000_000, 100_000, 10_000, key="pl_t2")
else:
pl_time = st.slider("์‹œ๊ฐ„ (๋…„)", 1_000_000, 100_000_000, 10_000_000, 1_000_000, key="pl_t3")
pl_flood = st.slider("๋ฒ”๋žŒ ๋นˆ๋„", 0.1, 1.0, 0.5, 0.1, key="pl_flood")
params = {'flood_freq': pl_flood}
with c2:
result = simulate_plain(PLAIN_THEORIES[pl_theory]['key'], pl_time, params, grid_size=grid_size)
# Shared Plot Container
plot_container = st.empty()
st.metric("ํ‰์•ผ ์œ ํ˜•", result['type'])
v_mode = st.radio("๋ณด๊ธฐ ๋ชจ๋“œ", ["๐Ÿ“Š ์‹œ๋ฎฌ๋ ˆ์ด์…˜ (2D)", "๐ŸŽฎ ์ธํ„ฐ๋ž™ํ‹ฐ๋ธŒ 3D", "๐Ÿ›ฐ๏ธ ์ฐธ๊ณ  ์‚ฌ์ง„"], horizontal=True, key="pl_v")
if "2D" in v_mode:
fig = render_terrain_3d(result['elevation'], f"ํ‰์•ผ - {pl_theory} ({pl_time:,}๋…„)", add_water=True, water_level=15)
plot_container.pyplot(fig)
plt.close()
elif "3D" in v_mode:
st.caption("๐Ÿ–ฑ๏ธ **๋งˆ์šฐ์Šค ๋“œ๋ž˜๊ทธ๋กœ ํšŒ์ „, ์Šคํฌ๋กค๋กœ ์คŒ**")
plotly_fig = render_terrain_plotly(result['elevation'], f"{result['type']} | {pl_time:,}๋…„", add_water=True, water_level=15)
plot_container.plotly_chart(plotly_fig, use_container_width=True, key="pl_plot_shared")
else:
st.info("์ค€๋น„ ์ค‘์ž…๋‹ˆ๋‹ค.")
# ===== ์Šคํฌ๋ฆฝํŠธ ๋žฉ =====
with tab_script:
st.header("๐Ÿ’ป ์Šคํฌ๋ฆฝํŠธ ๋žฉ (Script Lab)")
st.markdown("---")
st.info("๐Ÿ’ก ํŒŒ์ด์ฌ ์ฝ”๋“œ๋กœ ๋‚˜๋งŒ์˜ ์ง€ํ˜• ์ƒ์„ฑ ์•Œ๊ณ ๋ฆฌ์ฆ˜์„ ์‹คํ—˜ํ•ด๋ณด์„ธ์š”!\n\n์‚ฌ์šฉ ๊ฐ€๋Šฅํ•œ ๋ณ€์ˆ˜: `elevation` (๊ณ ๋„), `grid` (์ง€ํ˜•๊ฐ์ฒด), `np` (NumPy), `dt` (์‹œ๊ฐ„), `hydro` (์ˆ˜๋ ฅ), `erosion` (์นจ์‹)")
col_code, col_view = st.columns([1, 1])
with col_code:
st.subheader("๐Ÿ“œ ์ฝ”๋“œ ์—๋””ํ„ฐ")
# ์˜ˆ์ œ ์Šคํฌ๋ฆฝํŠธ
example_scripts = {
"01. ์ดˆ๊ธฐํ™” (ํ‰์ง€)": """# 100x100 ํ‰์ง€ ์ƒ์„ฑ
# elevation: 2D numpy array (float)
elevation[:] = 0.0""",
"02. ์‚ฌ์ธํŒŒ ์–ธ๋•": """# ์‚ฌ์ธํŒŒ ํ˜•ํƒœ์˜ ์–ธ๋• ์ƒ์„ฑ
import numpy as np
rows, cols = elevation.shape
for r in range(rows):
# r(ํ–‰)์— ๋”ฐ๋ผ ๋†’์ด๊ฐ€ ๋ณ€ํ•จ
elevation[r, :] = np.sin(r / 10.0) * 20.0 + 20.0""",
"03. ๋žœ๋ค ๋…ธ์ด์ฆˆ": """# ๋ฌด์ž‘์œ„ ์ง€ํ˜• ์ƒ์„ฑ
import numpy as np
# 0 ~ 50m ์‚ฌ์ด์˜ ๋žœ๋ค ๋†’์ด
elevation[:] = np.random.rand(*elevation.shape) * 50.0""",
"04. ์นจ์‹ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ Loop": """# 500๋…„ ๋™์•ˆ ๊ฐ•์ˆ˜ ๋ฐ ์นจ์‹ ์‹œ๋ฎฌ๋ ˆ์ด์…˜
# *์ฃผ์˜: ๋ฐ˜๋ณต๋ฌธ์ด ๋งŽ์œผ๋ฉด ๋А๋ ค์งˆ ์ˆ˜ ์žˆ์Šต๋‹ˆ๋‹ค.*
import numpy as np
# 1. ์ดˆ๊ธฐ ์ง€ํ˜• ์„ค์ • (๊ฒฝ์‚ฌ๋ฉด)
rows, cols = elevation.shape
if np.max(elevation) < 1.0: # ํ‰์ง€๋ผ๋ฉด ์ดˆ๊ธฐํ™”
for r in range(rows):
elevation[r, :] = 50.0 - (r/rows)*50.0
# 2. ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ๋ฃจํ”„ (100 step)
steps = 50
for i in range(steps):
# ๊ฐ•์ˆ˜ ๋ฐ ์œ ๋Ÿ‰ ๊ณ„์‚ฐ (Precipitation=0.05)
discharge = hydro.route_flow_d8(precipitation=0.05)
# ํ•˜์ฒœ ์นจ์‹ (Stream Power)
erosion.stream_power_erosion(discharge, dt=1.0)
# ์ง„ํ–‰์ƒํ™ฉ ์ถœ๋ ฅ (๋งˆ์ง€๋ง‰๋งŒ)
if i == steps - 1:
print(f"Simulation done: {steps} steps")
"""
}
selected_example = st.selectbox("์˜ˆ์ œ ์ฝ”๋“œ ์„ ํƒ", list(example_scripts.keys()))
default_code = example_scripts[selected_example]
user_script = st.text_area("Python Script", value=default_code, height=500, key="editor")
if st.button("๐Ÿš€ ์Šคํฌ๋ฆฝํŠธ ์‹คํ–‰ (Run)", type="primary"):
# 1. ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™” (๊ธฐ์กด session_state ์‚ฌ์šฉ or ์ƒˆ๋กœ ์ƒ์„ฑ)
if 'script_grid' not in st.session_state:
st.session_state['script_grid'] = WorldGrid(100, 100, 10.0)
grid_obj = st.session_state['script_grid']
executor = ScriptExecutor(grid_obj)
with st.spinner("์ฝ”๋“œ๋ฅผ ์‹คํ–‰ ์ค‘์ž…๋‹ˆ๋‹ค..."):
# ์‹คํ–‰ ์‹œ์ž‘ ์‹œ๊ฐ„
start_t = time.time()
success, msg = executor.execute(user_script)
end_t = time.time()
if success:
st.success(f"โœ… ์‹คํ–‰ ์„ฑ๊ณต ({end_t - start_t:.3f}s)")
if msg != "์‹คํ–‰ ์„ฑ๊ณต":
st.info(f"๋ฉ”์‹œ์ง€: {msg}")
# ๊ฒฐ๊ณผ ๊ฐฑ์‹  ํŠธ๋ฆฌ๊ฑฐ
st.session_state['script_run_count'] = st.session_state.get('script_run_count', 0) + 1
else:
st.error(f"โŒ ์‹คํ–‰ ์˜ค๋ฅ˜:\n{msg}")
with col_view:
st.subheader("๐Ÿ‘€ ๊ฒฐ๊ณผ ๋ทฐ์–ด")
# Grid ๊ฐ์ฒด ๊ฐ€์ ธ์˜ค๊ธฐ
if 'script_grid' not in st.session_state:
st.session_state['script_grid'] = WorldGrid(100, 100, 10.0)
grid_show = st.session_state['script_grid']
# ์‹œ๊ฐํ™” ์˜ต์…˜
show_water = st.checkbox("๋ฌผ ํ‘œ์‹œ (ํ•ด์ˆ˜๋ฉด 0m)", value=True)
# 3D ๋ Œ๋”๋ง
fig = render_terrain_plotly(
grid_show.elevation,
"Script Result",
add_water=show_water,
water_level=0.0
)
st.plotly_chart(fig, use_container_width=True)
# ํ†ต๊ณ„ ์ •๋ณด
st.markdown(f"""
**์ง€ํ˜• ํ†ต๊ณ„:**
- ์ตœ๋Œ€ ๊ณ ๋„: `{grid_show.elevation.max():.2f} m`
- ์ตœ์†Œ ๊ณ ๋„: `{grid_show.elevation.min():.2f} m`
- ํ‰๊ท  ๊ณ ๋„: `{grid_show.elevation.mean():.2f} m`
""")
if st.button("๐Ÿ”„ ๊ทธ๋ฆฌ๋“œ ์ดˆ๊ธฐํ™” (Reset)"):
st.session_state['script_grid'] = WorldGrid(100, 100, 10.0)
st.experimental_rerun()
else:
st.image("assets/reference/peneplain_erosion_cycle_1765436750353.png", caption="ํ‰์•ผ - ์ค€ํ‰์›ํ™” ๊ณผ์ • (AI ์ƒ์„ฑ)", use_column_width=True)
# ===== Project Genesis (Unified Engine) =====
with tab_genesis:
st.header("๐ŸŒ Project Genesis: Unified Earth Engine")
st.info("๋‹จ์ผ ๋ฌผ๋ฆฌ ์—”์ง„์œผ๋กœ ๋ชจ๋“  ์ง€ํ˜•์„ ์ƒ์„ฑํ•˜๋Š” ํ†ตํ•ฉ ์‹œ๋ฎฌ๋ ˆ์ด์…˜์ž…๋‹ˆ๋‹ค.")
c1, c2 = st.columns([1, 2])
with c1:
st.subheader("โš™๏ธ ์‹œ์Šคํ…œ ์ œ์–ด")
# 1. ์‹œ๋‚˜๋ฆฌ์˜ค ์„ ํƒ (Initial Conditions)
scenario = st.selectbox("์‹œ๋‚˜๋ฆฌ์˜ค ์ดˆ๊ธฐํ™”",
["Flat Plain (ํ‰์ง€)", "Sloped Terrain (๊ฒฝ์‚ฌ์ง€)", "Mountainous (์‚ฐ์ง€)"])
if st.button("๐Ÿ”„ ์—”์ง„ ์ดˆ๊ธฐํ™” (Reset)"):
# Initialize Grid
grid_gen = WorldGrid(width=grid_size, height=grid_size, cell_size=1000.0/grid_size)
# Apply Scenario
if scenario == "Sloped Terrain (๊ฒฝ์‚ฌ์ง€)":
rows, cols = grid_size, grid_size
for r in range(rows):
grid_gen.bedrock[r, :] = 100.0 - (r/rows)*50.0 # N->S Slope
elif scenario == "Mountainous (์‚ฐ์ง€)":
grid_gen.bedrock[:] = np.random.rand(grid_size, grid_size) * 50.0 + 50.0
else:
grid_gen.bedrock[:] = 10.0 # Flat
grid_gen.update_elevation()
# Create Engine
st.session_state['genesis_engine'] = EarthSystem(grid_gen)
st.success(f"{scenario} ์ดˆ๊ธฐํ™” ์™„๋ฃŒ")
st.markdown("---")
st.subheader("โ›ˆ๏ธ ๊ธฐํ›„ & ์ง€๊ตฌ์กฐ (Processes)")
gen_precip = st.slider("๊ฐ•์ˆ˜๋Ÿ‰ (Precipitation)", 0.0, 0.2, 0.05, 0.01)
gen_uplift = st.slider("์œต๊ธฐ์œจ (Uplift Rate)", 0.0, 2.0, 0.1, 0.1)
gen_diff = st.slider("์‚ฌ๋ฉด ํ™•์‚ฐ (Diffusion)", 0.0, 0.1, 0.01, 0.001)
# Kernel Toggles (Phase 2)
st.markdown("---")
st.subheader("๐Ÿงฉ ์ปค๋„ ์ œ์–ด (Process Toggles)")
col_k1, col_k2 = st.columns(2)
with col_k1:
k_lateral = st.checkbox("์ธก๋ฐฉ ์นจ์‹ (Lateral)", True, help="๊ณก๋ฅ˜ ํ˜•์„ฑ")
k_mass = st.checkbox("๋งค์Šค๋ฌด๋ธŒ๋จผํŠธ (Mass)", True, help="์‚ฐ์‚ฌํƒœ")
k_wave = st.checkbox("ํŒŒ๋ž‘ (Wave)", False, help="ํ•ด์•ˆ ์ง€ํ˜•")
with col_k2:
k_glacier = st.checkbox("๋น™ํ•˜ (Glacier)", False, help="U์ž๊ณก")
k_wind = st.checkbox("๋ฐ”๋žŒ (Wind)", False, help="์‚ฌ๊ตฌ")
st.markdown("---")
run_steps = st.slider("์‹คํ–‰ ์Šคํ… ์ˆ˜", 10, 200, 50, 10)
if st.button("โ–ถ๏ธ ์‹œ๋ฎฌ๋ ˆ์ด์…˜ ์‹คํ–‰ (Run Step)"):
if 'genesis_engine' not in st.session_state:
st.error("์—”์ง„์„ ๋จผ์ € ์ดˆ๊ธฐํ™”ํ•ด์ฃผ์„ธ์š”.")
else:
engine = st.session_state['genesis_engine']
progress_bar = st.progress(0)
for i in range(run_steps):
# Construct Settings with kernel toggles
settings = {
'uplift_rate': gen_uplift * 0.01,
'precipitation': gen_precip,
'diffusion_rate': gen_diff,
'lateral_erosion': k_lateral,
'mass_movement': k_mass,
# Note: Wave/Glacier/Wind require manual step call
}
engine.step(dt=1.0, settings=settings)
# Optional kernel steps
if k_wave:
engine.wave.step(dt=1.0)
if k_glacier:
engine.glacier.step(dt=1.0)
if k_wind:
engine.wind.step(dt=1.0)
progress_bar.progress((i+1)/run_steps)
st.success(f"{run_steps} ์Šคํ… ์‹คํ–‰ ์™„๋ฃŒ (Total Time: {engine.time:.1f})")
with c2:
st.subheader("๐Ÿ‘€ ์‹ค์‹œ๊ฐ„ ๊ด€์ธก (Observation)")
if 'genesis_engine' in st.session_state:
engine = st.session_state['genesis_engine']
state = engine.get_state()
# ํƒญ์œผ๋กœ ๋ทฐ ๋ชจ๋“œ ๋ถ„๋ฆฌ
view_type = st.radio("๋ ˆ์ด์–ด ์„ ํƒ", ["Composite (์ง€ํ˜•+๋ฌผ)", "Hydrology (์œ ๋Ÿ‰)", "Sediment (ํ‡ด์ ์ธต)"], horizontal=True)
if view_type == "Composite (์ง€ํ˜•+๋ฌผ)":
fig = render_terrain_plotly(state['elevation'],
f"Genesis Engine | T={engine.time:.1f}",
add_water=True, water_depth_grid=state['water_depth'],
sediment_grid=state['sediment'],
force_camera=True)
st.plotly_chart(fig, use_container_width=True)
elif view_type == "Hydrology (์œ ๋Ÿ‰)":
# Proper colormap for discharge
fig_hydro, ax_hydro = plt.subplots(figsize=(8, 6))
log_q = np.log1p(state['discharge'])
im = ax_hydro.imshow(log_q, cmap='Blues', origin='upper')
ax_hydro.set_title(f"์œ ๋Ÿ‰ ๋ถ„ํฌ (Log Scale) | T={engine.time:.1f}")
ax_hydro.set_xlabel("X (์…€)")
ax_hydro.set_ylabel("Y (์…€)")
plt.colorbar(im, ax=ax_hydro, label="Log(Q+1)")
st.pyplot(fig_hydro)
plt.close(fig_hydro)
# Stats
st.caption(f"์ตœ๋Œ€ ์œ ๋Ÿ‰: {state['discharge'].max():.1f} | ํ‰๊ท : {state['discharge'].mean():.2f}")
else:
# Proper colormap for sediment
fig_sed, ax_sed = plt.subplots(figsize=(8, 6))
im = ax_sed.imshow(state['sediment'], cmap='YlOrBr', origin='upper')
ax_sed.set_title(f"ํ‡ด์ ์ธต ๋‘๊ป˜ (m) | T={engine.time:.1f}")
ax_sed.set_xlabel("X (์…€)")
ax_sed.set_ylabel("Y (์…€)")
plt.colorbar(im, ax=ax_sed, label="ํ‡ด์ ์ธต (m)")
st.pyplot(fig_sed)
plt.close(fig_sed)
# Stats
st.caption(f"์ตœ๋Œ€ ํ‡ด์ : {state['sediment'].max():.2f}m | ์ด๋Ÿ‰: {state['sediment'].sum():.0f}mยณ")
else:
st.info("์ขŒ์ธก ํŒจ๋„์—์„œ ์—”์ง„์„ ์ดˆ๊ธฐํ™”ํ•˜์„ธ์š”.")
st.markdown("---")
st.caption("๐ŸŒ Geo-Lab AI v6.0 | Unified Earth System Project Genesis")
if __name__ == "__main__":
main()