h
File size: 3,221 Bytes
4cf972e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
from __future__ import annotations
import numpy as np
from dataclasses import dataclass
from typing import Optional, Dict

GAMMA_W = 9.81  # kN/m^3 (unit weight of water)

@dataclass
class Priors:
    N: int = 2000
    c_mu: float = 20.0
    c_sd: float = 5.0
    phi_mu: float = 30.0
    phi_sd: float = 5.0
    gamma_mu: float = 19.0
    gamma_sd: float = 1.0
    z_mu: float = 5.0
    z_sd: float = 1.0
    ru_mu: float = 0.2
    ru_sd: float = 0.05

@dataclass
class Targets:
    pf_target: float = 0.05

def compute_slope_deg(dem: np.ndarray, pixel_size: Optional[float] = None) -> np.ndarray:
    gy, gx = np.gradient(dem.astype(np.float32)) if pixel_size is None else np.gradient(dem.astype(np.float32), pixel_size)
    slope_rad = np.arctan(np.hypot(gx, gy))
    return np.degrees(slope_rad).astype(np.float32)

def pf_and_prescriptions(
    slope_deg: np.ndarray,
    priors: Priors = Priors(),
    targets: Targets = Targets(),
    mask: Optional[np.ndarray] = None,
    seed: Optional[int] = 42,
):
    assert slope_deg.ndim == 2, "slope_deg must be 2D"
    rng = np.random.default_rng(seed)

    th = np.radians(np.clip(slope_deg, 0, 89.9)).astype(np.float32)
    cos_th = np.cos(th)[..., None]
    sin_th = np.sin(th)[..., None]

    N = priors.N
    c = rng.normal(priors.c_mu, priors.c_sd, N)  # kPa
    phi = np.radians(rng.normal(priors.phi_mu, priors.phi_sd, N))  # rad
    gamma = rng.normal(priors.gamma_mu, priors.gamma_sd, N)  # kN/m^3
    z = rng.normal(priors.z_mu, priors.z_sd, N)  # m
    ru = np.clip(rng.normal(priors.ru_mu, priors.ru_sd, N), 0, 1)

    A = (gamma * z)[None, None, :]
    Acos2 = A * (cos_th ** 2)
    u = (ru * GAMMA_W * z)[None, None, :] * (cos_th ** 2)

    numerator = c[None, None, :] + (Acos2 - u) * np.tan(phi)[None, None, :]
    denominator = A * (sin_th * cos_th)
    denominator = np.maximum(denominator, 1e-6)

    FoS = numerator / denominator
    Pf = (FoS < 1.0).mean(axis=-1).astype(np.float32)

    # cohesion prescription
    delta_c_per_sample = np.maximum((1.0 - FoS) * denominator, 0.0)
    q_index = np.clip(int(np.ceil((1.0 - targets.pf_target) * N)) - 1, 0, N - 1)
    delta_c_sorted = np.partition(delta_c_per_sample, q_index, axis=-1)
    delta_c_needed = delta_c_sorted[..., q_index].astype(np.float32)

    # friction prescription
    Aeff = Acos2 - u
    deltaN = np.maximum((1.0 - FoS) * denominator, 0.0)
    tan_phi = np.tan(phi)[None, None, :]

    with np.errstate(divide='ignore', invalid='ignore'):
        needed_delta_tan = np.where(Aeff > 1e-6, deltaN / Aeff, np.inf)
        target_tan = tan_phi + needed_delta_tan
        target_phi = np.arctan(target_tan)
        delta_phi_per_sample = np.degrees(np.maximum(target_phi - phi[None, None, :], 0.0)).astype(np.float32)

    delta_phi_sorted = np.partition(delta_phi_per_sample, q_index, axis=-1)
    delta_phi_needed = delta_phi_sorted[..., q_index].astype(np.float32)

    if mask is not None:
        Pf = np.where(mask, Pf, np.nan)
        delta_c_needed = np.where(mask, delta_c_needed, np.nan)
        delta_phi_needed = np.where(mask, delta_phi_needed, np.nan)

    return {
        "pf": Pf,
        "delta_c_kpa": delta_c_needed,
        "delta_phi_deg": delta_phi_needed,
    }