File size: 3,048 Bytes
7c3bfa9
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
"""Helpers for building and interpreting theta(z) / u(z) profiles."""

from __future__ import annotations

from dataclasses import dataclass, field
from typing import List

import numpy as np

G = 9.80665


def default_profile_heights(zdom_km: float = 10.0, n_points: int = 9) -> np.ndarray:
    """Return ``n_points`` evenly spaced heights (meters) from 0 to ``zdom_km`` km."""
    return np.linspace(0.0, zdom_km * 1000.0, n_points)


def default_u_profile(zs: np.ndarray, u_surface: float = 20.0, shear: float = 0.5) -> np.ndarray:
    """A gently sheared wind profile: ``u(z) = u_surface + shear * z_km``."""
    zs_km = zs / 1000.0
    return u_surface + shear * zs_km


def default_theta_profile(
    zs: np.ndarray,
    theta_surface: float = 290.0,
    lapse_lower: float = 3.0,
    lapse_upper: float = 6.0,
    interface_km: float = 3.5,
) -> np.ndarray:
    """A two-regime potential-temperature profile.

    Stability contrast across ``interface_km`` mirrors the trapped-wave case
    in the original MATLAB example: weaker stability below, stronger above.
    """
    zs_km = zs / 1000.0
    theta = np.empty_like(zs_km)
    for i, zkm in enumerate(zs_km):
        if zkm <= interface_km:
            theta[i] = theta_surface + lapse_lower * zkm
        else:
            theta[i] = (
                theta_surface
                + lapse_lower * interface_km
                + lapse_upper * (zkm - interface_km)
            )
    return theta


def brunt_vaisala(z: np.ndarray, theta: np.ndarray) -> np.ndarray:
    """Finite-difference Brunt–Väisälä frequency squared (s⁻²)."""
    z = np.asarray(z, dtype=float)
    theta = np.asarray(theta, dtype=float)
    n = z.size
    n2 = np.empty(n)
    for i in range(n):
        if i == 0:
            dthdz = (theta[1] - theta[0]) / (z[1] - z[0])
        elif i == n - 1:
            dthdz = (theta[-1] - theta[-2]) / (z[-1] - z[-2])
        else:
            dthdz = (theta[i + 1] - theta[i - 1]) / (z[i + 1] - z[i - 1])
        n2[i] = (G / theta[i]) * dthdz
    return n2


def scorer_from_profile(z, u, theta) -> np.ndarray:
    """Wrapper around the reference implementation for use in the UI."""
    from .reference import scorer_from_profile as _imp
    return _imp(z, u, theta)


@dataclass
class WaveProfile:
    """Container for an edited profile displayed in the Dash app."""

    z: np.ndarray = field(default_factory=lambda: default_profile_heights())
    u: np.ndarray = field(default_factory=lambda: default_u_profile(default_profile_heights()))
    theta: np.ndarray = field(default_factory=lambda: default_theta_profile(default_profile_heights()))

    def as_lists(self) -> dict:
        return {"z": list(map(float, self.z)), "u": list(map(float, self.u)), "theta": list(map(float, self.theta))}

    @classmethod
    def from_lists(cls, store: dict) -> "WaveProfile":
        return cls(
            z=np.asarray(store["z"], dtype=float),
            u=np.asarray(store["u"], dtype=float),
            theta=np.asarray(store["theta"], dtype=float),
        )