File size: 5,787 Bytes
87602e0
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
"""Updraft/downdraft field prescription from a 1-D parcel model."""

from __future__ import annotations

import numpy as np
from scipy.interpolate import CubicSpline

from .sounding import G, lift_parcel


def diagnose_w_profile(
    z: np.ndarray,
    T_env: np.ndarray,
    qv_env: np.ndarray,
    p_hPa: np.ndarray,
    delta_T_K: float = 2.0,
) -> dict:
    """Diagnose the updraft vertical velocity profile from a 1-D parcel model.

    The parcel is initialized with a surface temperature excess of ``delta_T_K``
    K above the environment.  Vertical velocity is computed from the cumulative
    integral of buoyancy:

        w²(z) = 2 · ∫₀ᶻ B(z') dz'     (zero w² is floor-clipped)

    Above the EL the buoyancy is negative; w decelerates to zero at the
    overshoot top z_top.

    Returns dict: w_z, B_z, LCL_m, LFC_m, EL_m, CAPE, CIN, z_top_m.
    """
    parcel = lift_parcel(z, T_env, qv_env, p_hPa, delta_T_K)
    B = parcel["B"]
    EL_m = parcel["EL_m"]

    # Cumulative KE: w² = 2 · ∫₀ᶻ B dz
    # scipy.integrate.cumulative_trapezoid returns N-1 values; prepend 0
    from scipy.integrate import cumulative_trapezoid as cumtrapz
    ke2 = np.empty_like(z)
    ke2[0] = 0.0
    ke2[1:] = 2.0 * cumtrapz(B, z)
    ke2 = np.maximum(ke2, 0.0)
    w_z = np.sqrt(ke2)

    # Above EL: continue the integration with negative B until w → 0
    el_idx = int(np.searchsorted(z, EL_m))
    w_el_sq = ke2[el_idx] if el_idx < len(z) else 0.0
    z_top_m = EL_m
    if el_idx < len(z) - 1:
        w_sq_above = w_el_sq + 2.0 * cumtrapz(B[el_idx:], z[el_idx:], initial=0.0)
        w_sq_above = np.maximum(w_sq_above, 0.0)
        zero_cross = np.where(w_sq_above <= 0.0)[0]
        if len(zero_cross) > 0:
            first_zero = el_idx + zero_cross[0]
            z_top_m = float(z[first_zero])
            w_z[first_zero:] = 0.0
        else:
            # Overshoot extends beyond domain top; continue deceleration profile
            w_z[el_idx:] = np.sqrt(w_sq_above)
            z_top_m = float(z[-1])

    return {
        "w_z": w_z,
        "B_z": B,
        "LCL_m": parcel["LCL_m"],
        "LFC_m": parcel["LFC_m"],
        "EL_m": EL_m,
        "CAPE": parcel["CAPE"],
        "CIN": parcel["CIN"],
        "z_top_m": z_top_m,
        "T_parcel": parcel["T_parcel"],
    }


def tophat_profile(r: np.ndarray, r0: float, rolloff_frac: float = 0.10) -> np.ndarray:
    """Radial shape: uniform core, cosine taper in outer ``rolloff_frac`` fraction."""
    r = np.asarray(r, dtype=float)
    r_inner = r0 * (1.0 - rolloff_frac)
    out = np.where(r <= r_inner, 1.0, 0.0)
    taper_mask = (r > r_inner) & (r <= r0)
    out = np.where(
        taper_mask,
        0.5 * (1.0 + np.cos(np.pi * (r - r_inner) / (r0 - r_inner))),
        out,
    )
    return out


def cosine_profile(r: np.ndarray, r0: float) -> np.ndarray:
    """Radial shape: cosine bell w(r) = cos(π r / 2r₀) for r < r₀."""
    r = np.asarray(r, dtype=float)
    return np.where(r < r0, np.cos(0.5 * np.pi * r / r0), 0.0)


def build_updraft_fields(
    X: np.ndarray,
    Y: np.ndarray,
    z: np.ndarray,
    r0: float,
    shape: str,
    w_z: np.ndarray,
    zeta_cpts: np.ndarray,
    zeta_z_km: np.ndarray,
    env_u: np.ndarray,
    env_v: np.ndarray,
    theta_env: np.ndarray,
    theta_parcel: np.ndarray,
) -> dict:
    """Construct 3-D updraft fields on the (Nx, Ny, Nz) grid.

    Parameters
    ----------
    X, Y      : (Nx, Ny) horizontal coordinate arrays (meters)
    z         : (Nz,) height array (meters)
    r0        : updraft radius (m)
    shape     : 'tophat' or 'cosine'
    w_z       : (Nz,) diagnosed vertical velocity profile (m/s)
    zeta_cpts : (Ncpts,) prescribed vorticity values (s⁻¹) representing
                accumulated rotation from tilting/stretching in a mature storm
    zeta_z_km : (Ncpts,) heights of control points (km)
    env_u, env_v     : (Nz,) environmental wind components (m/s)
    theta_env, theta_parcel : (Nz,) potential temperature arrays (K)

    Returns dict of 3-D arrays: w3d, u3d, v3d, zeta3d, theta_prime3d.
    """
    Nx, Ny = X.shape
    Nz = len(z)

    # Radial distance from domain center
    xc = X.mean()
    yc = Y.mean()
    R = np.sqrt((X - xc) ** 2 + (Y - yc) ** 2)  # (Nx, Ny)

    # Radial shape function
    if shape == "tophat":
        shape_fn = tophat_profile(R, r0)
    else:
        shape_fn = cosine_profile(R, r0)

    # Interpolate prescribed vorticity control points to full z grid
    if len(zeta_cpts) >= 2:
        z_cpts_m = np.asarray(zeta_z_km) * 1000.0
        cs = CubicSpline(z_cpts_m, np.asarray(zeta_cpts), extrapolate=True)
        zeta_z = cs(z)
    else:
        zeta_z = np.zeros(Nz)

    # Build 3-D fields
    w3d = np.empty((Nx, Ny, Nz))
    u3d = np.empty((Nx, Ny, Nz))
    v3d = np.empty((Nx, Ny, Nz))
    zeta3d = np.zeros((Nx, Ny, Nz))
    theta_prime3d = np.zeros((Nx, Ny, Nz))

    # Azimuthal angle from center
    phi = np.arctan2(Y - yc, X - xc)  # (Nx, Ny)

    for k in range(Nz):
        w3d[:, :, k] = shape_fn * w_z[k]

        # Solid-body rotation: v_θ = ζ(z) * r / 2
        v_theta = zeta_z[k] * R / 2.0
        u_core = -v_theta * np.sin(phi)
        v_core = v_theta * np.cos(phi)

        # Apply radial taper to the rotational wind too
        u3d[:, :, k] = env_u[k] + shape_fn * u_core
        v3d[:, :, k] = env_v[k] + shape_fn * v_core

        # Vertical vorticity within core: ζ_z ≈ shape_fn * zeta_z[k]
        zeta3d[:, :, k] = shape_fn * zeta_z[k]

        # Potential temperature perturbation within core
        theta_prime3d[:, :, k] = shape_fn * (theta_parcel[k] - theta_env[k])

    return {
        "w3d": w3d,
        "u3d": u3d,
        "v3d": v3d,
        "zeta3d": zeta3d,
        "theta_prime3d": theta_prime3d,
    }