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" | |
| } | |
| } | |
| # ============ ์๋ฎฌ๋ ์ด์ ํจ์๋ค ============ | |
| 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} | |
| 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 | |
| } | |
| 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} | |
| 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 | |
| } | |
| 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 | |
| } | |
| 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 | |
| } | |
| 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} | |
| 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 | |
| } | |
| 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} | |
| 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} | |
| 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} | |
| 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} | |
| 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} | |
| 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)"} | |
| 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)"} | |
| 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} | |
| 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)"} | |
| 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)"} | |
| 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} | |
| 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)"} | |
| 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)"} | |
| 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} | |
| 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() | |