Geo-Lab / engine /wind.py
HANSOL
Geo-Lab v4.1 - Clean deployment
2afa69c
"""
Wind Kernel (๋ฐ”๋žŒ ์ปค๋„)
ํ’์„ฑ ์ง€ํ˜• ํ˜•์„ฑ ํ”„๋กœ์„ธ์Šค
- ํ’์‹ (Deflation): ๋ฏธ์„ธ ์ž…์ž ์ œ๊ฑฐ
- ๋งˆ์‹ (Abrasion): ๋ชจ๋ž˜ ์ถฉ๋Œ์— ์˜ํ•œ ์นจ์‹
- ํ’์  (Aeolian Deposition): ์‚ฌ๊ตฌ ํ˜•์„ฑ
ํ•ต์‹ฌ:
- ๋ฐ”๋žŒ ์†๋„์— ๋น„๋ก€ํ•œ ์šด๋ฐ˜๋ ฅ
- ์ž…์ž ํฌ๊ธฐ์— ๋”ฐ๋ฅธ ์„ ํƒ์  ์šด๋ฐ˜
"""
import numpy as np
from .grid import WorldGrid
class WindKernel:
"""
๋ฐ”๋žŒ ์ปค๋„
๊ฑด์กฐ ์ง€์—ญ์—์„œ์˜ ํ’์‹๊ณผ ํ’์ ์„ ์‹œ๋ฎฌ๋ ˆ์ด์…˜.
"""
def __init__(self, grid: WorldGrid,
wind_speed: float = 10.0, # m/s
wind_direction: float = 45.0, # degrees from N
K_erosion: float = 0.0001,
sand_threshold: float = 0.1): # ์‚ฌ๊ตฌ ํ˜•์„ฑ ์ž„๊ณ„ ํ‡ด์ ๋Ÿ‰
self.grid = grid
self.wind_speed = wind_speed
self.wind_direction = np.radians(wind_direction)
self.K = K_erosion
self.sand_threshold = sand_threshold
def get_wind_vector(self) -> tuple:
"""
๋ฐ”๋žŒ ๋ฐฉํ–ฅ ๋ฒกํ„ฐ ๋ฐ˜ํ™˜
Returns:
(dy, dx): ๋ฐ”๋žŒ ๋ฐฉํ–ฅ ๋‹จ์œ„ ๋ฒกํ„ฐ
"""
dx = np.sin(self.wind_direction)
dy = -np.cos(self.wind_direction) # Y์ถ• ๋ฐ˜์ „ (ํ™”๋ฉด ์ขŒํ‘œ๊ณ„)
return dy, dx
def calculate_transport_capacity(self,
vegetation_cover: np.ndarray = None) -> np.ndarray:
"""
๋ชจ๋ž˜ ์šด๋ฐ˜๋ ฅ ๊ณ„์‚ฐ
Args:
vegetation_cover: ์‹์ƒ ํ”ผ๋ณต๋ฅ  (0~1, ๋†’์œผ๋ฉด ์šด๋ฐ˜๋ ฅ ๊ฐ์†Œ)
Returns:
capacity: ์šด๋ฐ˜๋ ฅ ๋ฐฐ์—ด
"""
h, w = self.grid.height, self.grid.width
# ๊ธฐ๋ณธ ์šด๋ฐ˜๋ ฅ = ํ’์†^3 (๊ฒฝํ—˜์‹)
base_capacity = (self.wind_speed ** 3) * self.K
capacity = np.ones((h, w)) * base_capacity
# ์‹์ƒ ํšจ๊ณผ (์žˆ์œผ๋ฉด ์šด๋ฐ˜๋ ฅ ๊ฐ์†Œ)
if vegetation_cover is not None:
capacity *= (1 - vegetation_cover)
# ๊ฒฝ์‚ฌ ํšจ๊ณผ (๋ฐ”๋žŒ๋ฐ›์ด vs ๋ฐ”๋žŒ๊ทธ๋Š˜)
slope, aspect = self.grid.get_gradient()
# ๋ฐ”๋žŒ๋ฐ›์ด = ํ’ํ–ฅ๊ณผ ๋ฐ˜๋Œ€ ๊ฒฝ์‚ฌ๋ฉด โ†’ ๊ฐ์† โ†’ ํ‡ด์ 
# ๋ฐ”๋žŒ๊ทธ๋Š˜ = ํ’ํ–ฅ๊ณผ ๊ฐ™์€ ๊ฒฝ์‚ฌ๋ฉด โ†’ ๊ฐ€์† โ†’ ์นจ์‹
dy, dx = self.get_wind_vector()
# ๊ฒฝ์‚ฌ๋ฉด์˜ ๋…ธ์ถœ๋„ (๋ฐ”๋žŒ๊ณผ ๊ฒฝ์‚ฌ ๋ฐฉํ–ฅ์˜ ๋‚ด์ )
exposure = dx * np.gradient(self.grid.elevation, axis=1) + \
dy * np.gradient(self.grid.elevation, axis=0)
# ๋…ธ์ถœ๋„์— ๋”ฐ๋ฅธ ์šด๋ฐ˜๋ ฅ ์กฐ์ •
capacity *= (1 + 0.5 * np.clip(exposure, -1, 1))
# ํ•ด์ˆ˜๋ฉด ์•„๋ž˜๋Š” 0
capacity[self.grid.is_underwater()] = 0
return np.maximum(capacity, 0)
def deflation(self, capacity: np.ndarray, dt: float = 1.0) -> np.ndarray:
"""
ํ’์‹ (Deflation) - ๋ฏธ์„ธ ์ž…์ž ์ œ๊ฑฐ
Args:
capacity: ์šด๋ฐ˜๋ ฅ ๋ฐฐ์—ด
dt: ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ
Returns:
erosion: ์นจ์‹๋Ÿ‰ ๋ฐฐ์—ด
"""
h, w = self.grid.height, self.grid.width
# ์นจ์‹๋Ÿ‰ = ์šด๋ฐ˜๋ ฅ * dt (ํ‡ด์ ์ธต์—์„œ๋งŒ)
available = self.grid.sediment
erosion = np.minimum(capacity * dt, available)
# ํ‡ด์ ์ธต ๊ฐ์†Œ
self.grid.sediment -= erosion
return erosion
def transport_and_deposit(self,
eroded_material: np.ndarray,
capacity: np.ndarray,
dt: float = 1.0) -> np.ndarray:
"""
ํ’์  (Aeolian Deposition) - ์‚ฌ๊ตฌ ํ˜•์„ฑ
Args:
eroded_material: ์นจ์‹๋œ ๋ฌผ์งˆ๋Ÿ‰
capacity: ์šด๋ฐ˜๋ ฅ ๋ฐฐ์—ด
dt: ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ
Returns:
deposition: ํ‡ด์ ๋Ÿ‰ ๋ฐฐ์—ด
"""
h, w = self.grid.height, self.grid.width
dy, dx = self.get_wind_vector()
# ๋ฌผ์งˆ ์ด๋™
deposition = np.zeros((h, w), dtype=np.float64)
for r in range(h):
for c in range(w):
if eroded_material[r, c] <= 0:
continue
# ๋ฐ”๋žŒ ๋ฐฉํ–ฅ์œผ๋กœ ์ด๋™
tr = int(r + dy * 2) # 2์…€ ์ด๋™
tc = int(c + dx * 2)
if not (0 <= tr < h and 0 <= tc < w):
continue
# ๋ชฉํ‘œ ์ง€์ ์˜ ์šด๋ฐ˜๋ ฅ ํ™•์ธ
if capacity[tr, tc] < capacity[r, c]:
# ์šด๋ฐ˜๋ ฅ ๊ฐ์†Œ โ†’ ํ‡ด์ 
deposit_amount = eroded_material[r, c] * (1 - capacity[tr, tc] / capacity[r, c])
deposition[tr, tc] += deposit_amount
else:
# ๊ณ„์† ์šด๋ฐ˜ (๋‹ค์Œ ์…€๋กœ)
# ๊ฐ„๋‹จํžˆ ์œ„ํ•ด ์ผ๋ถ€๋งŒ ํ‡ด์ 
deposition[tr, tc] += eroded_material[r, c] * 0.1
# ํ‡ด์  ์ ์šฉ
self.grid.add_sediment(deposition)
return deposition
def form_barchan(self, iteration: int = 5):
"""
๋ฐ”๋ฅดํ•œ ์‚ฌ๊ตฌ ํ˜•์„ฑ (๋ฐ˜๋ณต ์‹œ๋ฎฌ๋ ˆ์ด์…˜)
๋ฐ”๋žŒ๋ฐ›์ด: ์™„๊ฒฝ์‚ฌ, ๋ฐ”๋žŒ๊ทธ๋Š˜: ๊ธ‰๊ฒฝ์‚ฌ (Slip Face)
Args:
iteration: ํ˜•ํƒœ ๋‹ค๋“ฌ๊ธฐ ๋ฐ˜๋ณต ํšŸ์ˆ˜
"""
h, w = self.grid.height, self.grid.width
dy, dx = self.get_wind_vector()
# ์‚ฌ๊ตฌ ํ›„๋ณด (ํ‡ด์ ๋ฌผ ๋งŽ์€ ๊ณณ)
dune_mask = self.grid.sediment > self.sand_threshold
for _ in range(iteration):
# ๋ฐ”๋žŒ๋ฐ›์ด ์ชฝ ์™„๋งŒํ•˜๊ฒŒ
for r in range(1, h - 1):
for c in range(1, w - 1):
if not dune_mask[r, c]:
continue
# ๋ฐ”๋žŒ๋ฐ›์ด ์ด์›ƒ
wr, wc = int(r - dy), int(c - dx)
if 0 <= wr < h and 0 <= wc < w:
# ๊ฒฝ์‚ฌ ์™„ํ™”
avg = (self.grid.sediment[r, c] + self.grid.sediment[wr, wc]) / 2
self.grid.sediment[r, c] = self.grid.sediment[r, c] * 0.9 + avg * 0.1
self.grid.update_elevation()
def step(self, vegetation_cover: np.ndarray = None,
dt: float = 1.0) -> dict:
"""
1๋‹จ๊ณ„ ๋ฐ”๋žŒ ์ž‘์šฉ ์‹คํ–‰
Args:
vegetation_cover: ์‹์ƒ ํ”ผ๋ณต๋ฅ 
dt: ์‹œ๊ฐ„ ๊ฐ„๊ฒฉ
Returns:
result: ์นจ์‹/ํ‡ด์  ๊ฒฐ๊ณผ
"""
# 1. ์šด๋ฐ˜๋ ฅ ๊ณ„์‚ฐ
capacity = self.calculate_transport_capacity(vegetation_cover)
# 2. ํ’์‹
erosion = self.deflation(capacity, dt)
# 3. ์ด๋™ ๋ฐ ํ‡ด์ 
deposition = self.transport_and_deposit(erosion, capacity, dt)
return {
'erosion': erosion,
'deposition': deposition,
'capacity': capacity
}