Spaces:
Sleeping
Major UI overhaul: Skew-T, 4-tab layout, branding, bug fixes
Browse files- Add full Skew-T log-P diagram with parcel trace, CAPE/CIN shading,
wind barbs (NH convention), and level markers (LCL/LFC/EL/Top)
- Restructure UI into Inputs / Outputs / Getting Started / Theory tabs;
Inputs uses 2-col grid (sounding+updraft left, hodograph+zeta right)
- Move ζ(z) profile editor below hodograph; rename to "Prescribed Mesocyclone ζ(z)"
- Fix draggable circle oval bug: handle both xanchor and x0/x1 relay keys;
add missing zeta-profile redraw callback
- Unify all diverging colormaps to RdBu_r
- Add UIUC CliMAS logo, cloud SVG favicon, updated page title/subtitle
- Fix Greek character rendering: remove text-transform:uppercase from section titles
- Label pressure reference lines on Skew-T; fix y-axis range (1025–98 hPa)
- Change "Weak shear" preset to "Linear shear"
- Add LaTeX equations and Model Assumptions table to Theory page
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
@@ -132,6 +132,7 @@ def _run_model(snd_params, upd_params, u_pts, v_pts, zeta_cpts):
|
|
| 132 |
"p_buoy": p_buoy,
|
| 133 |
**accels,
|
| 134 |
"snd": snd,
|
|
|
|
| 135 |
"w_z": wp["w_z"],
|
| 136 |
"B_z": wp["B_z"],
|
| 137 |
"EL_m": wp["EL_m"],
|
|
@@ -171,8 +172,8 @@ def _slider(id_, label, mn, mx, step, value, unit="", tip=""):
|
|
| 171 |
)
|
| 172 |
|
| 173 |
|
| 174 |
-
def
|
| 175 |
-
"""
|
| 176 |
values = np.asarray(values, dtype=float)
|
| 177 |
z_km = np.asarray(z_km, dtype=float)
|
| 178 |
vlo, vhi = xrange
|
|
@@ -205,8 +206,14 @@ def _profile_editor(fig_id, title, values, z_km, xunit, xrange):
|
|
| 205 |
yaxis=dict(range=[0, z_km[-1] + 0.5], fixedrange=True),
|
| 206 |
shapes=shapes,
|
| 207 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
return dcc.Graph(
|
| 209 |
-
id=fig_id,
|
|
|
|
| 210 |
config={"edits": {"shapePosition": True}, "displayModeBar": False, "scrollZoom": False},
|
| 211 |
)
|
| 212 |
|
|
@@ -668,6 +675,283 @@ def _theory_content():
|
|
| 668 |
])
|
| 669 |
|
| 670 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 671 |
# ---------------------------------------------------------------------------
|
| 672 |
# Layout
|
| 673 |
# ---------------------------------------------------------------------------
|
|
@@ -675,160 +959,190 @@ def _theory_content():
|
|
| 675 |
def _build_layout():
|
| 676 |
diag = _run_model(SND_DEFAULTS, UPD_DEFAULTS, WK_U, WK_V, ZETA_DEFAULTS)
|
| 677 |
|
| 678 |
-
#
|
| 679 |
-
|
| 680 |
-
|
| 681 |
-
|
| 682 |
-
|
| 683 |
-
|
| 684 |
-
|
| 685 |
-
|
| 686 |
-
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
| 702 |
-
|
| 703 |
-
|
| 704 |
-
|
| 705 |
-
|
| 706 |
-
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
]),
|
| 712 |
-
])),
|
| 713 |
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
|
| 728 |
-
|
| 729 |
-
|
| 730 |
-
|
| 731 |
-
|
| 732 |
-
|
| 733 |
-
|
| 734 |
-
|
| 735 |
-
|
| 736 |
-
|
| 737 |
-
|
| 738 |
-
|
| 739 |
-
|
| 740 |
-
|
| 741 |
-
|
| 742 |
-
|
| 743 |
-
|
| 744 |
-
|
| 745 |
-
|
| 746 |
-
|
| 747 |
-
|
| 748 |
-
|
| 749 |
-
|
| 750 |
-
|
| 751 |
-
|
| 752 |
-
|
| 753 |
-
|
| 754 |
-
|
| 755 |
-
|
| 756 |
-
|
| 757 |
-
|
| 758 |
-
|
| 759 |
-
|
| 760 |
-
|
| 761 |
-
|
| 762 |
-
|
| 763 |
-
|
| 764 |
-
|
| 765 |
-
|
| 766 |
-
|
| 767 |
-
|
| 768 |
-
|
| 769 |
-
|
| 770 |
-
|
| 771 |
-
|
| 772 |
-
|
| 773 |
-
|
| 774 |
-
|
| 775 |
-
|
| 776 |
-
|
| 777 |
-
|
| 778 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 779 |
]),
|
| 780 |
])
|
| 781 |
|
| 782 |
-
|
| 783 |
-
|
| 784 |
-
|
| 785 |
-
|
| 786 |
-
|
| 787 |
-
|
| 788 |
-
|
| 789 |
-
|
| 790 |
-
|
| 791 |
-
|
| 792 |
-
|
| 793 |
-
|
| 794 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 795 |
]),
|
|
|
|
| 796 |
]),
|
| 797 |
-
html.Div(className="
|
| 798 |
-
html.
|
| 799 |
-
dcc.
|
| 800 |
-
|
| 801 |
-
|
| 802 |
-
|
| 803 |
-
style={"color": "#6ecbff", "fontSize": "12px", "minWidth": "80px"}),
|
| 804 |
]),
|
| 805 |
-
dcc.Graph(id="main-heatmap", config={"displayModeBar": False}),
|
| 806 |
-
html.Div(className="uf-section-title", style={"marginTop": "14px"},
|
| 807 |
-
children="Diagnosed w(z) from parcel model"),
|
| 808 |
-
dcc.Graph(id="w-profile-graph", config={"displayModeBar": False}),
|
| 809 |
])
|
| 810 |
|
| 811 |
-
|
| 812 |
-
|
| 813 |
-
|
| 814 |
-
|
| 815 |
-
|
| 816 |
-
|
| 817 |
-
|
| 818 |
-
|
|
|
|
| 819 |
])
|
| 820 |
|
| 821 |
-
model_layout = html.Div(className="uf-main", children=[left, center, right])
|
| 822 |
-
|
| 823 |
return html.Div(className="uf-root", children=[
|
| 824 |
html.Div(className="uf-header", children=[
|
| 825 |
-
html.
|
| 826 |
-
|
| 827 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 828 |
]),
|
| 829 |
-
dcc.Tabs(id="page-tabs", value="
|
| 830 |
-
dcc.Tab(label="
|
| 831 |
-
children=
|
|
|
|
|
|
|
| 832 |
dcc.Tab(label="Getting Started", value="help", className="uf-ptab", selected_className="uf-ptab-sel",
|
| 833 |
children=_getting_started_content()),
|
| 834 |
dcc.Tab(label="Theory", value="theory", className="uf-ptab", selected_className="uf-ptab-sel",
|
|
@@ -889,12 +1203,25 @@ def _register_callbacks(app):
|
|
| 889 |
return no_update
|
| 890 |
u = list(store["u"]); v = list(store["v"]); changed = False
|
| 891 |
for i in range(len(u)):
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 898 |
return {"u": u, "v": v} if changed else no_update
|
| 899 |
|
| 900 |
@app.callback(
|
|
@@ -908,12 +1235,14 @@ def _register_callbacks(app):
|
|
| 908 |
return no_update
|
| 909 |
zeta = list(store["zeta"]); changed = False
|
| 910 |
for i in range(len(zeta)):
|
| 911 |
-
|
| 912 |
-
|
|
|
|
|
|
|
|
|
|
| 913 |
try:
|
| 914 |
-
zeta[i] = max(-0.10, min(0.10, float(relay[
|
| 915 |
-
except (TypeError, ValueError):
|
| 916 |
-
pass
|
| 917 |
return {"zeta": zeta} if changed else no_update
|
| 918 |
|
| 919 |
@app.callback(
|
|
@@ -927,6 +1256,13 @@ def _register_callbacks(app):
|
|
| 927 |
lm_u=d.get("lm_u"), lm_v=d.get("lm_v"),
|
| 928 |
mean_u=d.get("mean_u"), mean_v=d.get("mean_v"))
|
| 929 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 930 |
@app.callback(
|
| 931 |
Output("model-rev", "data"),
|
| 932 |
[Input("snd-theta-ml", "value"), Input("snd-qv-ml", "value"),
|
|
@@ -1054,6 +1390,13 @@ def _register_callbacks(app):
|
|
| 1054 |
]
|
| 1055 |
return html.Tbody(rows)
|
| 1056 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1057 |
@app.callback(
|
| 1058 |
[Output("buoy-profile", "figure"), Output("accel-profile", "figure")],
|
| 1059 |
Input("model-rev", "data"),
|
|
@@ -1103,7 +1446,7 @@ body { background: #0b0e14; color: #dfe3ea; font-family: -apple-system, system-u
|
|
| 1103 |
.uf-center{ background: #11161f; border-radius: 8px; padding: 12px; }
|
| 1104 |
.uf-right { background: #11161f; border-radius: 8px; padding: 12px; }
|
| 1105 |
|
| 1106 |
-
.uf-section-title { font-size: 11px; font-weight: 600; color: #8d97a2; letter-spacing: 0.
|
| 1107 |
.uf-slider-row { margin-bottom: 10px; }
|
| 1108 |
.uf-slider-header { display: flex; justify-content: space-between; align-items: center; font-size: 12px; margin-bottom: 2px; }
|
| 1109 |
.uf-slider-label { color: #c7ced6; }
|
|
@@ -1173,15 +1516,35 @@ table tr:nth-child(even) td { background: #161d29; }
|
|
| 1173 |
.assume-check { color: #5ac87a; font-size: 13px; padding: 5px 10px 5px 6px; width: 18px; vertical-align: top; }
|
| 1174 |
.assume-text { color: #dfe3ea; font-weight: 600; padding: 5px 12px 5px 4px; width: 310px; vertical-align: top; }
|
| 1175 |
.assume-note { color: #8d97a2; padding: 5px 4px; line-height: 1.5; vertical-align: top; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1176 |
"""
|
| 1177 |
|
| 1178 |
|
| 1179 |
def create_app() -> Dash:
|
| 1180 |
-
|
| 1181 |
-
|
|
|
|
|
|
|
| 1182 |
app.layout = _build_layout()
|
| 1183 |
app.index_string = f"""<!doctype html><html><head>
|
| 1184 |
-
{{%metas%}}<title>{{%title%}}</title>
|
|
|
|
|
|
|
| 1185 |
<style>{_CSS}</style>
|
| 1186 |
</head><body>{{%app_entry%}}<footer>{{%config%}}{{%scripts%}}{{%renderer%}}</footer></body></html>"""
|
| 1187 |
_register_callbacks(app)
|
|
|
|
| 132 |
"p_buoy": p_buoy,
|
| 133 |
**accels,
|
| 134 |
"snd": snd,
|
| 135 |
+
"T_parcel": wp["T_parcel"],
|
| 136 |
"w_z": wp["w_z"],
|
| 137 |
"B_z": wp["B_z"],
|
| 138 |
"EL_m": wp["EL_m"],
|
|
|
|
| 172 |
)
|
| 173 |
|
| 174 |
|
| 175 |
+
def _profile_editor_fig(title, values, z_km, xunit, xrange):
|
| 176 |
+
"""Return a go.Figure for a draggable profile editor."""
|
| 177 |
values = np.asarray(values, dtype=float)
|
| 178 |
z_km = np.asarray(z_km, dtype=float)
|
| 179 |
vlo, vhi = xrange
|
|
|
|
| 206 |
yaxis=dict(range=[0, z_km[-1] + 0.5], fixedrange=True),
|
| 207 |
shapes=shapes,
|
| 208 |
)
|
| 209 |
+
return fig
|
| 210 |
+
|
| 211 |
+
|
| 212 |
+
def _profile_editor(fig_id, title, values, z_km, xunit, xrange):
|
| 213 |
+
"""Draggable profile graph (Mountain Waves pattern)."""
|
| 214 |
return dcc.Graph(
|
| 215 |
+
id=fig_id,
|
| 216 |
+
figure=_profile_editor_fig(title, values, z_km, xunit, xrange),
|
| 217 |
config={"edits": {"shapePosition": True}, "displayModeBar": False, "scrollZoom": False},
|
| 218 |
)
|
| 219 |
|
|
|
|
| 675 |
])
|
| 676 |
|
| 677 |
|
| 678 |
+
# ---------------------------------------------------------------------------
|
| 679 |
+
# Skew-T helpers
|
| 680 |
+
# ---------------------------------------------------------------------------
|
| 681 |
+
|
| 682 |
+
_SKEW = 45.0 # °C shift per log10-pressure decade
|
| 683 |
+
_BARB_X = 60.0 # x anchor for wind barb tips (skewed °C)
|
| 684 |
+
_BARB_ASPECT = 120.0 # x-units per 1 log10-p unit (matches ~550px / ~4.5px/unit plot)
|
| 685 |
+
_BARB_STAFF = 5.0
|
| 686 |
+
_BARB_FULL = 2.8
|
| 687 |
+
_BARB_HALF = 1.4
|
| 688 |
+
_BARB_SEP = 0.91
|
| 689 |
+
|
| 690 |
+
|
| 691 |
+
def _sx(T_C, p_hPa):
|
| 692 |
+
"""Skew-T x coordinate."""
|
| 693 |
+
return np.asarray(T_C, float) + _SKEW * np.log10(1000.0 / np.asarray(p_hPa, float))
|
| 694 |
+
|
| 695 |
+
|
| 696 |
+
def _wind_barbs_trace(env_u, env_v, p_snd):
|
| 697 |
+
"""
|
| 698 |
+
Return (xs, ys) lists (with None separators) for a single wind-barb
|
| 699 |
+
go.Scatter trace. NH convention: staff points upwind; flags on the right
|
| 700 |
+
when looking from tip toward tail (CW 90° from the upwind direction).
|
| 701 |
+
"""
|
| 702 |
+
barb_lvls = [1000, 925, 850, 700, 600, 500, 400, 300, 250, 200, 150, 100]
|
| 703 |
+
# np.interp requires xp increasing; p_snd decreases, so reverse
|
| 704 |
+
p_rev = p_snd[::-1]
|
| 705 |
+
u_rev = env_u[::-1]
|
| 706 |
+
v_rev = env_v[::-1]
|
| 707 |
+
|
| 708 |
+
xs: list = []
|
| 709 |
+
ys: list = []
|
| 710 |
+
|
| 711 |
+
def screen(ex, en, length):
|
| 712 |
+
"""(east, north) direction + length → (Δx_skewed, Δlog10p)."""
|
| 713 |
+
return ex * length, -en * length / _BARB_ASPECT
|
| 714 |
+
|
| 715 |
+
for p_tgt in barb_lvls:
|
| 716 |
+
if p_tgt > p_rev[-1] or p_tgt < p_rev[0]:
|
| 717 |
+
continue
|
| 718 |
+
u = float(np.interp(p_tgt, p_rev, u_rev))
|
| 719 |
+
v = float(np.interp(p_tgt, p_rev, v_rev))
|
| 720 |
+
spd = float(np.hypot(u, v))
|
| 721 |
+
spd_kt = spd * 1.9438
|
| 722 |
+
log_p0 = np.log10(float(p_tgt))
|
| 723 |
+
|
| 724 |
+
if spd < 0.5: # calm: tiny tick
|
| 725 |
+
xs.extend([_BARB_X - 0.4, _BARB_X + 0.4, None])
|
| 726 |
+
ys.extend([p_tgt, p_tgt, None])
|
| 727 |
+
continue
|
| 728 |
+
|
| 729 |
+
ux = -u / spd; uy = -v / spd # upwind unit vector (E, N components)
|
| 730 |
+
pex = uy; pen = -ux # perpendicular = CW 90° of upwind (NH convention)
|
| 731 |
+
|
| 732 |
+
# Staff (tip → tail)
|
| 733 |
+
dx_s, dl_s = screen(ux, uy, _BARB_STAFF)
|
| 734 |
+
xs.extend([_BARB_X, _BARB_X + dx_s, None])
|
| 735 |
+
ys.extend([float(p_tgt), float(10 ** (log_p0 + dl_s)), None])
|
| 736 |
+
|
| 737 |
+
n50 = int(spd_kt // 50)
|
| 738 |
+
rem = spd_kt - n50 * 50
|
| 739 |
+
n10 = int(rem // 10); rem -= n10 * 10
|
| 740 |
+
n5 = 1 if rem >= 5 else 0
|
| 741 |
+
|
| 742 |
+
pos = _BARB_STAFF # current position along staff from tip
|
| 743 |
+
for _ in range(n50): # pennants
|
| 744 |
+
dx_b, dl_b = screen(ux, uy, pos)
|
| 745 |
+
b1x, b1l = _BARB_X + dx_b, log_p0 + dl_b
|
| 746 |
+
dx_e, dl_e = screen(pex, pen, _BARB_FULL)
|
| 747 |
+
b2x, b2l = b1x + dx_e, b1l + dl_e
|
| 748 |
+
dx_b3, dl_b3 = screen(ux, uy, pos - 2 * _BARB_SEP)
|
| 749 |
+
b3x, b3l = _BARB_X + dx_b3, log_p0 + dl_b3
|
| 750 |
+
xs.extend([b1x, b2x, b3x, b1x, None])
|
| 751 |
+
ys.extend([10**b1l, 10**b2l, 10**b3l, 10**b1l, None])
|
| 752 |
+
pos -= 2 * _BARB_SEP
|
| 753 |
+
for _ in range(n10): # full barbs
|
| 754 |
+
dx_b, dl_b = screen(ux, uy, pos)
|
| 755 |
+
b1x, b1l = _BARB_X + dx_b, log_p0 + dl_b
|
| 756 |
+
dx_e, dl_e = screen(pex, pen, _BARB_FULL)
|
| 757 |
+
xs.extend([b1x, b1x + dx_e, None])
|
| 758 |
+
ys.extend([10**b1l, 10**(b1l + dl_e), None])
|
| 759 |
+
pos -= _BARB_SEP
|
| 760 |
+
if n5: # half barb
|
| 761 |
+
dx_b, dl_b = screen(ux, uy, pos)
|
| 762 |
+
b1x, b1l = _BARB_X + dx_b, log_p0 + dl_b
|
| 763 |
+
dx_e, dl_e = screen(pex, pen, _BARB_HALF)
|
| 764 |
+
xs.extend([b1x, b1x + dx_e, None])
|
| 765 |
+
ys.extend([10**b1l, 10**(b1l + dl_e), None])
|
| 766 |
+
|
| 767 |
+
return xs, ys
|
| 768 |
+
|
| 769 |
+
|
| 770 |
+
def _skewt_figure() -> go.Figure:
|
| 771 |
+
"""Build a Skew-T log-P diagram from the current _C cache."""
|
| 772 |
+
snd = _C.get("snd")
|
| 773 |
+
if snd is None:
|
| 774 |
+
return go.Figure()
|
| 775 |
+
|
| 776 |
+
Rd = 287.04; Rv = 461.5; Lv = 2.501e6; Cp = 1005.7
|
| 777 |
+
eps = Rd / Rv; KAPPA = Rd / Cp
|
| 778 |
+
|
| 779 |
+
# Background pressure grid (surface → top, decreasing)
|
| 780 |
+
p_bg = np.concatenate([np.arange(1050, 500, -5.0), np.arange(500, 95, -2.5)])
|
| 781 |
+
|
| 782 |
+
fig = go.Figure()
|
| 783 |
+
|
| 784 |
+
# ---- isotherms ----
|
| 785 |
+
for T_iso in range(-100, 61, 10):
|
| 786 |
+
fig.add_trace(go.Scatter(
|
| 787 |
+
x=_sx(T_iso, p_bg), y=p_bg, mode="lines", hoverinfo="skip", showlegend=False,
|
| 788 |
+
line=dict(color="rgba(90,120,150,0.55)" if T_iso == 0 else "rgba(65,90,115,0.3)",
|
| 789 |
+
width=0.9 if T_iso == 0 else 0.55)))
|
| 790 |
+
|
| 791 |
+
# ---- dry adiabats ----
|
| 792 |
+
for theta_K in [255, 265, 275, 285, 295, 305, 315, 325, 340, 360, 390]:
|
| 793 |
+
T_C = theta_K * (p_bg / 1000.0) ** KAPPA - 273.15
|
| 794 |
+
fig.add_trace(go.Scatter(
|
| 795 |
+
x=_sx(T_C, p_bg), y=p_bg, mode="lines", hoverinfo="skip", showlegend=False,
|
| 796 |
+
line=dict(color="rgba(210,155,55,0.38)", width=0.6, dash="dash")))
|
| 797 |
+
|
| 798 |
+
# ---- moist adiabats ----
|
| 799 |
+
def _ma(T0_C, p_arr):
|
| 800 |
+
T = T0_C + 273.15
|
| 801 |
+
out = []
|
| 802 |
+
for i, p in enumerate(p_arr):
|
| 803 |
+
out.append(T - 273.15)
|
| 804 |
+
if i < len(p_arr) - 1:
|
| 805 |
+
dp = p_arr[i + 1] - p
|
| 806 |
+
T_c = T - 273.15
|
| 807 |
+
es = 6.112 * np.exp(17.67 * T_c / (T_c + 243.5))
|
| 808 |
+
rs = eps * es / max(p - es, 0.1)
|
| 809 |
+
# dT/dp = (T/p) * (Rd + Lv*rs/T) / (Cp + Lv²*rs/(Rv*T²))
|
| 810 |
+
T = max(T + (T / p) * (Rd + Lv * rs / T) /
|
| 811 |
+
(Cp + Lv**2 * rs / (Rv * T**2)) * dp, 100.0)
|
| 812 |
+
return np.array(out)
|
| 813 |
+
|
| 814 |
+
for T0 in [-25, -15, -5, 5, 15, 25, 35]:
|
| 815 |
+
T_ma = _ma(T0, p_bg)
|
| 816 |
+
mask = T_ma > -85
|
| 817 |
+
if mask.sum() >= 3:
|
| 818 |
+
fig.add_trace(go.Scatter(
|
| 819 |
+
x=_sx(T_ma[mask], p_bg[mask]), y=p_bg[mask], mode="lines",
|
| 820 |
+
hoverinfo="skip", showlegend=False,
|
| 821 |
+
line=dict(color="rgba(70,185,115,0.40)", width=0.6, dash="dash")))
|
| 822 |
+
|
| 823 |
+
# ---- mixing-ratio lines (below 550 hPa) ----
|
| 824 |
+
p_low = p_bg[p_bg >= 550]
|
| 825 |
+
for w_gkg in [2, 4, 7, 10, 16]:
|
| 826 |
+
w = w_gkg / 1000.0
|
| 827 |
+
e = p_low * w / (eps + w)
|
| 828 |
+
ln_e = np.log(np.maximum(e, 1e-6) / 6.112)
|
| 829 |
+
T_C = 243.5 * ln_e / (17.67 - ln_e)
|
| 830 |
+
mask = (T_C > -40) & (T_C < 40)
|
| 831 |
+
if mask.sum() >= 2:
|
| 832 |
+
fig.add_trace(go.Scatter(
|
| 833 |
+
x=_sx(T_C[mask], p_low[mask]), y=p_low[mask], mode="lines",
|
| 834 |
+
hoverinfo="skip", showlegend=False,
|
| 835 |
+
line=dict(color="rgba(65,150,215,0.40)", width=0.5, dash="dot")))
|
| 836 |
+
|
| 837 |
+
# ---- sounding profiles ----
|
| 838 |
+
p_snd = snd["p_hPa"]
|
| 839 |
+
T_env_C = snd["T_K"] - 273.15
|
| 840 |
+
Td_env_C= snd["Td_K"] - 273.15
|
| 841 |
+
|
| 842 |
+
mask_snd = (p_snd >= 98) & (p_snd <= 1060)
|
| 843 |
+
p_s = p_snd[mask_snd]
|
| 844 |
+
T_s = T_env_C[mask_snd]
|
| 845 |
+
Td_s = Td_env_C[mask_snd]
|
| 846 |
+
|
| 847 |
+
T_pcl_K = _C.get("T_parcel")
|
| 848 |
+
T_pcl_C = (T_pcl_K[mask_snd] - 273.15) if T_pcl_K is not None else None
|
| 849 |
+
|
| 850 |
+
diag = _C.get("diag", {})
|
| 851 |
+
def _z2p(z_m):
|
| 852 |
+
return float(np.interp(float(z_m or 0), Z_GRID, p_snd))
|
| 853 |
+
|
| 854 |
+
p_LCL = _z2p(diag.get("LCL_m", 0))
|
| 855 |
+
p_LFC = _z2p(diag.get("LFC_m", 0))
|
| 856 |
+
p_EL = _z2p(_C.get("EL_m", 0))
|
| 857 |
+
p_top = _z2p(_C.get("z_top_m", 0))
|
| 858 |
+
|
| 859 |
+
# CAPE shading
|
| 860 |
+
if T_pcl_C is not None:
|
| 861 |
+
cm = (p_s <= p_LFC + 5) & (p_s >= p_EL - 5) & (T_pcl_C > T_s)
|
| 862 |
+
if cm.sum() >= 2:
|
| 863 |
+
xenv, xpcl = _sx(T_s[cm], p_s[cm]), _sx(T_pcl_C[cm], p_s[cm])
|
| 864 |
+
fig.add_trace(go.Scatter(
|
| 865 |
+
x=list(xenv) + list(xpcl[::-1]), y=list(p_s[cm]) + list(p_s[cm][::-1]),
|
| 866 |
+
fill="toself", fillcolor="rgba(50,200,70,0.22)",
|
| 867 |
+
line=dict(width=0), showlegend=False, hoverinfo="skip"))
|
| 868 |
+
# CIN shading
|
| 869 |
+
cm2 = (p_s >= p_LFC - 5) & (T_pcl_C < T_s)
|
| 870 |
+
if cm2.sum() >= 2:
|
| 871 |
+
xenv, xpcl = _sx(T_s[cm2], p_s[cm2]), _sx(T_pcl_C[cm2], p_s[cm2])
|
| 872 |
+
fig.add_trace(go.Scatter(
|
| 873 |
+
x=list(xenv) + list(xpcl[::-1]), y=list(p_s[cm2]) + list(p_s[cm2][::-1]),
|
| 874 |
+
fill="toself", fillcolor="rgba(180,180,180,0.18)",
|
| 875 |
+
line=dict(width=0), showlegend=False, hoverinfo="skip"))
|
| 876 |
+
|
| 877 |
+
fig.add_trace(go.Scatter(
|
| 878 |
+
x=_sx(T_s, p_s), y=p_s, mode="lines", name="Temp",
|
| 879 |
+
line=dict(color="#e06c6c", width=2.5),
|
| 880 |
+
hovertemplate="%{text}°C @ %{y:.0f} hPa<extra>T</extra>",
|
| 881 |
+
text=[f"{t:.1f}" for t in T_s]))
|
| 882 |
+
fig.add_trace(go.Scatter(
|
| 883 |
+
x=_sx(Td_s, p_s), y=p_s, mode="lines", name="Dewpt",
|
| 884 |
+
line=dict(color="#5ac85a", width=2.5),
|
| 885 |
+
hovertemplate="%{text}°C @ %{y:.0f} hPa<extra>Td</extra>",
|
| 886 |
+
text=[f"{t:.1f}" for t in Td_s]))
|
| 887 |
+
if T_pcl_C is not None:
|
| 888 |
+
fig.add_trace(go.Scatter(
|
| 889 |
+
x=_sx(T_pcl_C, p_s), y=p_s, mode="lines", name="Parcel",
|
| 890 |
+
line=dict(color="#ffd685", width=1.8, dash="dash"),
|
| 891 |
+
hovertemplate="%{text}°C @ %{y:.0f} hPa<extra>Parcel</extra>",
|
| 892 |
+
text=[f"{t:.1f}" for t in T_pcl_C]))
|
| 893 |
+
|
| 894 |
+
# Level lines
|
| 895 |
+
for p_lvl, label, color in [
|
| 896 |
+
(p_LCL, "LCL", "#9abfff"), (p_LFC, "LFC", "#ffaa44"),
|
| 897 |
+
(p_EL, "EL", "#ffd685"), (p_top, "Top", "#ff8c00"),
|
| 898 |
+
]:
|
| 899 |
+
if 98 < p_lvl < 1060:
|
| 900 |
+
fig.add_hline(y=p_lvl, line=dict(color=color, width=0.9, dash="dot"),
|
| 901 |
+
annotation_text=label, annotation_position="top left",
|
| 902 |
+
annotation_font=dict(color=color, size=10))
|
| 903 |
+
|
| 904 |
+
# Wind barbs
|
| 905 |
+
env_u = _C.get("env_u"); env_v = _C.get("env_v")
|
| 906 |
+
if env_u is not None and env_v is not None:
|
| 907 |
+
bx, by = _wind_barbs_trace(env_u, env_v, p_snd)
|
| 908 |
+
if bx:
|
| 909 |
+
fig.add_trace(go.Scatter(x=bx, y=by, mode="lines",
|
| 910 |
+
line=dict(color="#ccd3db", width=1.2),
|
| 911 |
+
showlegend=False, hoverinfo="skip"))
|
| 912 |
+
# Vertical guide line for barb column
|
| 913 |
+
fig.add_vline(x=_BARB_X - _BARB_STAFF - 0.5,
|
| 914 |
+
line=dict(color="rgba(80,100,120,0.3)", width=0.6))
|
| 915 |
+
|
| 916 |
+
p_ticks = [1000, 925, 850, 700, 600, 500, 400, 300, 250, 200, 150, 100]
|
| 917 |
+
x_lo = min(float(_sx(T_s.min() - 5, 1050)), -55.0)
|
| 918 |
+
x_hi = max(float(_sx(T_s.max() + 2, min(p_s))), _BARB_X + _BARB_FULL + 2)
|
| 919 |
+
|
| 920 |
+
fig.update_layout(
|
| 921 |
+
template="plotly_dark",
|
| 922 |
+
height=620,
|
| 923 |
+
margin=dict(l=60, r=15, t=18, b=40),
|
| 924 |
+
paper_bgcolor="#11161f",
|
| 925 |
+
plot_bgcolor="#0a0f1a",
|
| 926 |
+
xaxis=dict(
|
| 927 |
+
title="Temperature (°C)",
|
| 928 |
+
range=[x_lo, x_hi],
|
| 929 |
+
tickmode="array",
|
| 930 |
+
tickvals=list(range(-80, 61, 10)),
|
| 931 |
+
ticktext=[str(t) for t in range(-80, 61, 10)],
|
| 932 |
+
showgrid=False, zeroline=False,
|
| 933 |
+
),
|
| 934 |
+
yaxis=dict(
|
| 935 |
+
title="Pressure (hPa)",
|
| 936 |
+
type="log",
|
| 937 |
+
range=[np.log10(1025), np.log10(98)],
|
| 938 |
+
tickmode="array",
|
| 939 |
+
tickvals=p_ticks,
|
| 940 |
+
ticktext=[str(p) for p in p_ticks],
|
| 941 |
+
showgrid=False,
|
| 942 |
+
),
|
| 943 |
+
legend=dict(x=0.01, y=0.01, font=dict(size=11),
|
| 944 |
+
bgcolor="rgba(10,15,26,0.8)",
|
| 945 |
+
bordercolor="#2d3a4b", borderwidth=1),
|
| 946 |
+
)
|
| 947 |
+
for p_ref in [1000, 850, 700, 500, 300, 200]:
|
| 948 |
+
fig.add_hline(y=p_ref, line=dict(color="rgba(90,120,150,0.22)", width=0.6),
|
| 949 |
+
annotation_text=f"{p_ref}",
|
| 950 |
+
annotation_position="left",
|
| 951 |
+
annotation_font=dict(size=9, color="rgba(130,155,185,0.7)"))
|
| 952 |
+
return fig
|
| 953 |
+
|
| 954 |
+
|
| 955 |
# ---------------------------------------------------------------------------
|
| 956 |
# Layout
|
| 957 |
# ---------------------------------------------------------------------------
|
|
|
|
| 959 |
def _build_layout():
|
| 960 |
diag = _run_model(SND_DEFAULTS, UPD_DEFAULTS, WK_U, WK_V, ZETA_DEFAULTS)
|
| 961 |
|
| 962 |
+
# =========================================================
|
| 963 |
+
# INPUTS TAB — three columns: Sounding | Hodograph | Updraft
|
| 964 |
+
# =========================================================
|
| 965 |
+
snd_col = html.Div(className="inp-col", children=[
|
| 966 |
+
html.Div("Weisman-Klemp Sounding", className="uf-section-title"),
|
| 967 |
+
_slider("snd-theta-ml", "θ_ml (K)", 295, 315, 0.5, SND_DEFAULTS["theta_ml"],
|
| 968 |
+
tip="Mixed-layer potential temperature. Higher values give a warmer "
|
| 969 |
+
"boundary layer and more buoyancy."),
|
| 970 |
+
_slider("snd-qv-ml", "qv_ml (g/kg)", 8, 20, 0.5, SND_DEFAULTS["qv_ml"],
|
| 971 |
+
tip="Boundary-layer water vapor mixing ratio. More moisture lowers "
|
| 972 |
+
"the LCL and increases CAPE."),
|
| 973 |
+
_slider("snd-z-ml", "BL depth (m)", 500, 2000, 100, SND_DEFAULTS["z_ml"], " m",
|
| 974 |
+
tip="Depth of the well-mixed layer. The constant-θ and qv profile "
|
| 975 |
+
"extends to this height."),
|
| 976 |
+
_slider("snd-z-trop", "Tropopause ht. (m)", 9000, 14000, 250,
|
| 977 |
+
SND_DEFAULTS["z_trop"], " m",
|
| 978 |
+
tip="Height of the tropopause. A higher tropopause allows a deeper "
|
| 979 |
+
"updraft and more CAPE."),
|
| 980 |
+
_slider("snd-T-trop", "Tropopause T (K)", 195, 220, 1,
|
| 981 |
+
SND_DEFAULTS["T_trop"], " K",
|
| 982 |
+
tip="Temperature at the tropopause. Colder tropopause → larger "
|
| 983 |
+
"temperature difference from the surface → more CAPE."),
|
| 984 |
+
_slider("snd-gamma", "Lapse rate exp.", 0.8, 1.8, 0.05,
|
| 985 |
+
SND_DEFAULTS["gamma_ft"],
|
| 986 |
+
tip="Shape exponent γ for the free-troposphere θ profile: "
|
| 987 |
+
"θ = θ_ml + (θ_trop−θ_ml)·((z−z_ml)/(z_trop−z_ml))^γ. "
|
| 988 |
+
"γ=1 linear; γ<1 more unstable near surface; γ>1 near tropopause."),
|
| 989 |
+
html.Div(className="uf-preset-row", children=[
|
| 990 |
+
html.Button("WK Supercell", id="preset-wk", className="uf-btn"),
|
| 991 |
+
html.Button("Linear shear", id="preset-weak", className="uf-btn"),
|
| 992 |
+
html.Button("Reset", id="preset-reset", className="uf-btn"),
|
| 993 |
+
]),
|
| 994 |
+
])
|
|
|
|
|
|
|
| 995 |
|
| 996 |
+
hodo_col = html.Div(className="inp-col", style={"gridColumn": "2", "gridRow": "1 / span 2"}, children=[
|
| 997 |
+
html.Div("Environmental Hodograph", className="uf-section-title"),
|
| 998 |
+
dcc.Graph(id="hodograph",
|
| 999 |
+
figure=_hodo_figure(WK_U, WK_V,
|
| 1000 |
+
storm_u=diag["storm_u"], storm_v=diag["storm_v"],
|
| 1001 |
+
lm_u=diag["lm_u"], lm_v=diag["lm_v"],
|
| 1002 |
+
mean_u=diag["mean_u"], mean_v=diag["mean_v"]),
|
| 1003 |
+
config={"edits": {"shapePosition": True},
|
| 1004 |
+
"displayModeBar": False, "scrollZoom": False}),
|
| 1005 |
+
html.Div([
|
| 1006 |
+
html.Span("Drag gold circles to edit wind at each level. "
|
| 1007 |
+
"RM = right-mover, LM = left-mover, × = mean wind.",
|
| 1008 |
+
style={"color": "#8f98a3", "fontSize": "11px"}),
|
| 1009 |
+
_help("Wind levels: 0, 1, 2, 3, 4, 5, 6, 8, 10 km. "
|
| 1010 |
+
"Storm motion from Bunkers et al. (2000): "
|
| 1011 |
+
"mean 0–6 km wind ± 7.5 m/s ⊥ to shear vector. "
|
| 1012 |
+
"SRH computed relative to the right-mover."),
|
| 1013 |
+
], style={"display": "flex", "alignItems": "flex-start", "gap": "6px",
|
| 1014 |
+
"marginTop": "6px"}),
|
| 1015 |
+
html.Div([
|
| 1016 |
+
html.Div("Prescribed Mesocyclone ζ(z)",
|
| 1017 |
+
className="uf-section-title", style={"marginTop": "14px"}),
|
| 1018 |
+
_help("Vertical vorticity profile representing rotation the storm has built "
|
| 1019 |
+
"up. Drag circles rightward for cyclonic (positive) rotation. "
|
| 1020 |
+
"Drives p'_spin and updraft helicity."),
|
| 1021 |
+
], style={"display": "flex", "alignItems": "center", "gap": "6px"}),
|
| 1022 |
+
_profile_editor("zeta-profile", "ζ(z) (s⁻¹)",
|
| 1023 |
+
ZETA_DEFAULTS, ZETA_Z_KM, "s⁻¹", (-0.05, 0.05)),
|
| 1024 |
+
])
|
| 1025 |
+
|
| 1026 |
+
upd_col = html.Div(className="inp-col", children=[
|
| 1027 |
+
html.Div("Updraft Core", className="uf-section-title"),
|
| 1028 |
+
html.Div([
|
| 1029 |
+
html.Span("ΔT surface (K)", className="uf-slider-label"),
|
| 1030 |
+
_help("Temperature excess of the surface parcel above the environment. "
|
| 1031 |
+
"Drives the diagnosed w(z): w²(z) = max(0, 2·∫B dz). "
|
| 1032 |
+
"Larger ΔT → more KE to overcome CIN → stronger updraft."),
|
| 1033 |
+
dcc.Input(id="upd-delta-T", type="number", value=UPD_DEFAULTS["delta_T"],
|
| 1034 |
+
step=0.1, min=0.1, max=10.0,
|
| 1035 |
+
style={"width": "80px", "marginLeft": "8px",
|
| 1036 |
+
"background": "#0f1520", "border": "1px solid #2d3a4b",
|
| 1037 |
+
"color": "#dfe3ea", "padding": "4px 8px",
|
| 1038 |
+
"borderRadius": "4px"}),
|
| 1039 |
+
], style={"display": "flex", "alignItems": "center", "marginBottom": "10px"}),
|
| 1040 |
+
_slider("upd-r0", "Radius (m)", 500, 5000, 100, UPD_DEFAULTS["r0"], " m",
|
| 1041 |
+
tip="Updraft core radius. The radial shape function tapers w and ζ "
|
| 1042 |
+
"to zero at this distance from center."),
|
| 1043 |
+
html.Div([
|
| 1044 |
+
html.Span("Core shape", className="uf-slider-label"),
|
| 1045 |
+
_help("Top-hat: uniform w inside 90% of r₀ with cosine taper in the outer 10%. "
|
| 1046 |
+
"Cosine bell: w(r) = cos(π·r/2r₀), smooth with no flat core."),
|
| 1047 |
+
], style={"display": "flex", "alignItems": "center", "gap": "6px",
|
| 1048 |
+
"marginTop": "10px"}),
|
| 1049 |
+
dcc.RadioItems(
|
| 1050 |
+
id="upd-shape",
|
| 1051 |
+
options=[{"label": " Top-hat", "value": "tophat"},
|
| 1052 |
+
{"label": " Cosine", "value": "cosine"}],
|
| 1053 |
+
value=UPD_DEFAULTS["shape"],
|
| 1054 |
+
labelStyle={"marginRight": "14px", "color": "#dfe3ea", "fontSize": "13px"},
|
| 1055 |
+
style={"marginBottom": "10px"},
|
| 1056 |
+
),
|
| 1057 |
+
])
|
| 1058 |
+
|
| 1059 |
+
inputs_tab = html.Div(className="inp-grid", children=[snd_col, hodo_col, upd_col])
|
| 1060 |
+
|
| 1061 |
+
# =========================================================
|
| 1062 |
+
# OUTPUTS TAB — sub-tabs: Skew-T | Model Results
|
| 1063 |
+
# =========================================================
|
| 1064 |
+
skewt_subtab = html.Div(className="skewt-layout", children=[
|
| 1065 |
+
dcc.Graph(id="skewt-graph", figure=_skewt_figure(),
|
| 1066 |
+
config={"displayModeBar": False}),
|
| 1067 |
+
html.Div(className="skewt-side", children=[
|
| 1068 |
+
html.Div("Sounding Parameters", className="uf-section-title"),
|
| 1069 |
+
html.Table(id="diag-table", style={"width": "100%", "borderCollapse": "collapse"}),
|
| 1070 |
+
html.Div("Diagnosed w(z)", className="uf-section-title",
|
| 1071 |
+
style={"marginTop": "14px"}),
|
| 1072 |
+
dcc.Graph(id="w-profile-graph", config={"displayModeBar": False}),
|
| 1073 |
]),
|
| 1074 |
])
|
| 1075 |
|
| 1076 |
+
results_subtab = html.Div(className="results-layout", children=[
|
| 1077 |
+
html.Div(className="results-center", children=[
|
| 1078 |
+
html.Div(className="uf-field-controls", children=[
|
| 1079 |
+
dcc.Dropdown(
|
| 1080 |
+
id="field-select",
|
| 1081 |
+
options=[{"label": v, "value": k} for k, v in FIELD_LABELS.items()],
|
| 1082 |
+
value="w", clearable=False,
|
| 1083 |
+
style={"width": "240px", "fontSize": "13px", "color": "#111"},
|
| 1084 |
+
),
|
| 1085 |
+
dcc.Tabs(id="view-tabs", value="plan", className="uf-view-tabs", children=[
|
| 1086 |
+
dcc.Tab(label="Plan view", value="plan", className="uf-vtab", selected_className="uf-vtab-sel"),
|
| 1087 |
+
dcc.Tab(label="X cross-sec", value="xcross", className="uf-vtab", selected_className="uf-vtab-sel"),
|
| 1088 |
+
dcc.Tab(label="Y cross-sec", value="ycross", className="uf-vtab", selected_className="uf-vtab-sel"),
|
| 1089 |
+
]),
|
| 1090 |
+
]),
|
| 1091 |
+
html.Div(className="uf-slice-row", children=[
|
| 1092 |
+
html.Span("Slice: ", className="uf-slider-label"),
|
| 1093 |
+
dcc.Slider(id="slice-slider", min=0, max=NZ - 1, step=1, value=NZ // 4,
|
| 1094 |
+
marks=None, tooltip={"always_visible": False},
|
| 1095 |
+
className="uf-slice-slider"),
|
| 1096 |
+
html.Span(id="slice-label", children="z = 4.0 km",
|
| 1097 |
+
style={"color": "#6ecbff", "fontSize": "12px", "minWidth": "80px"}),
|
| 1098 |
]),
|
| 1099 |
+
dcc.Graph(id="main-heatmap", config={"displayModeBar": False}),
|
| 1100 |
]),
|
| 1101 |
+
html.Div(className="results-side", children=[
|
| 1102 |
+
html.Div("Buoyancy B(z)", className="uf-section-title"),
|
| 1103 |
+
dcc.Graph(id="buoy-profile", config={"displayModeBar": False}),
|
| 1104 |
+
html.Div("Vertical accelerations", className="uf-section-title",
|
| 1105 |
+
style={"marginTop": "10px"}),
|
| 1106 |
+
dcc.Graph(id="accel-profile", config={"displayModeBar": False}),
|
|
|
|
| 1107 |
]),
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1108 |
])
|
| 1109 |
|
| 1110 |
+
outputs_tab = html.Div(children=[
|
| 1111 |
+
dcc.Tabs(id="out-tabs", value="skewt", className="uf-tabs", children=[
|
| 1112 |
+
dcc.Tab(label="Skew-T", value="skewt",
|
| 1113 |
+
className="uf-tab", selected_className="uf-tab-sel",
|
| 1114 |
+
children=skewt_subtab),
|
| 1115 |
+
dcc.Tab(label="Model Results", value="results",
|
| 1116 |
+
className="uf-tab", selected_className="uf-tab-sel",
|
| 1117 |
+
children=results_subtab),
|
| 1118 |
+
]),
|
| 1119 |
])
|
| 1120 |
|
|
|
|
|
|
|
| 1121 |
return html.Div(className="uf-root", children=[
|
| 1122 |
html.Div(className="uf-header", children=[
|
| 1123 |
+
html.Div(style={"display": "flex", "alignItems": "center", "gap": "14px"}, children=[
|
| 1124 |
+
html.Span("🌩️", style={"fontSize": "32px", "lineHeight": "1"}),
|
| 1125 |
+
html.Div([
|
| 1126 |
+
html.H1("Updraft Forcing: A parcel-grid model",
|
| 1127 |
+
style={"margin": "0 0 2px 0", "fontSize": "22px"}),
|
| 1128 |
+
html.Div("A kinematic-diagnostic model to examine the effects of environmental wind shear and mesocyclone intensity on perturbation pressure and vertical accelerations",
|
| 1129 |
+
style={"color": "#9aa3ad", "fontSize": "12px"}),
|
| 1130 |
+
]),
|
| 1131 |
+
html.Div(style={"marginLeft": "auto"}, children=[
|
| 1132 |
+
html.A(href="https://climas.illinois.edu/", target="_blank",
|
| 1133 |
+
children=html.Img(
|
| 1134 |
+
src="/assets/climas_logo.jpg",
|
| 1135 |
+
alt="UIUC CliMAS",
|
| 1136 |
+
style={"height": "44px", "borderRadius": "8px",
|
| 1137 |
+
"display": "block"})),
|
| 1138 |
+
]),
|
| 1139 |
+
]),
|
| 1140 |
]),
|
| 1141 |
+
dcc.Tabs(id="page-tabs", value="inputs", className="uf-page-tabs", children=[
|
| 1142 |
+
dcc.Tab(label="Inputs", value="inputs", className="uf-ptab", selected_className="uf-ptab-sel",
|
| 1143 |
+
children=inputs_tab),
|
| 1144 |
+
dcc.Tab(label="Outputs", value="outputs", className="uf-ptab", selected_className="uf-ptab-sel",
|
| 1145 |
+
children=outputs_tab),
|
| 1146 |
dcc.Tab(label="Getting Started", value="help", className="uf-ptab", selected_className="uf-ptab-sel",
|
| 1147 |
children=_getting_started_content()),
|
| 1148 |
dcc.Tab(label="Theory", value="theory", className="uf-ptab", selected_className="uf-ptab-sel",
|
|
|
|
| 1203 |
return no_update
|
| 1204 |
u = list(store["u"]); v = list(store["v"]); changed = False
|
| 1205 |
for i in range(len(u)):
|
| 1206 |
+
# Plotly emits xanchor/yanchor for pixel-mode shapes; after conversion
|
| 1207 |
+
# it may emit x0/x1/y0/y1 in data coords — handle both.
|
| 1208 |
+
if f"shapes[{i}].xanchor" in relay:
|
| 1209 |
+
try:
|
| 1210 |
+
u[i] = max(-60.0, min(60.0, float(relay[f"shapes[{i}].xanchor"]))); changed = True
|
| 1211 |
+
except (TypeError, ValueError): pass
|
| 1212 |
+
elif f"shapes[{i}].x0" in relay and f"shapes[{i}].x1" in relay:
|
| 1213 |
+
try:
|
| 1214 |
+
u[i] = max(-60.0, min(60.0, (float(relay[f"shapes[{i}].x0"]) + float(relay[f"shapes[{i}].x1"])) / 2)); changed = True
|
| 1215 |
+
except (TypeError, ValueError): pass
|
| 1216 |
+
|
| 1217 |
+
if f"shapes[{i}].yanchor" in relay:
|
| 1218 |
+
try:
|
| 1219 |
+
v[i] = max(-60.0, min(60.0, float(relay[f"shapes[{i}].yanchor"]))); changed = True
|
| 1220 |
+
except (TypeError, ValueError): pass
|
| 1221 |
+
elif f"shapes[{i}].y0" in relay and f"shapes[{i}].y1" in relay:
|
| 1222 |
+
try:
|
| 1223 |
+
v[i] = max(-60.0, min(60.0, (float(relay[f"shapes[{i}].y0"]) + float(relay[f"shapes[{i}].y1"])) / 2)); changed = True
|
| 1224 |
+
except (TypeError, ValueError): pass
|
| 1225 |
return {"u": u, "v": v} if changed else no_update
|
| 1226 |
|
| 1227 |
@app.callback(
|
|
|
|
| 1235 |
return no_update
|
| 1236 |
zeta = list(store["zeta"]); changed = False
|
| 1237 |
for i in range(len(zeta)):
|
| 1238 |
+
if f"shapes[{i}].xanchor" in relay:
|
| 1239 |
+
try:
|
| 1240 |
+
zeta[i] = max(-0.10, min(0.10, float(relay[f"shapes[{i}].xanchor"]))); changed = True
|
| 1241 |
+
except (TypeError, ValueError): pass
|
| 1242 |
+
elif f"shapes[{i}].x0" in relay and f"shapes[{i}].x1" in relay:
|
| 1243 |
try:
|
| 1244 |
+
zeta[i] = max(-0.10, min(0.10, (float(relay[f"shapes[{i}].x0"]) + float(relay[f"shapes[{i}].x1"])) / 2)); changed = True
|
| 1245 |
+
except (TypeError, ValueError): pass
|
|
|
|
| 1246 |
return {"zeta": zeta} if changed else no_update
|
| 1247 |
|
| 1248 |
@app.callback(
|
|
|
|
| 1256 |
lm_u=d.get("lm_u"), lm_v=d.get("lm_v"),
|
| 1257 |
mean_u=d.get("mean_u"), mean_v=d.get("mean_v"))
|
| 1258 |
|
| 1259 |
+
@app.callback(
|
| 1260 |
+
Output("zeta-profile", "figure"),
|
| 1261 |
+
Input("zeta-store", "data"),
|
| 1262 |
+
)
|
| 1263 |
+
def _redraw_zeta(store):
|
| 1264 |
+
return _profile_editor_fig("ζ(z) (s⁻¹)", store["zeta"], ZETA_Z_KM, "s⁻¹", (-0.05, 0.05))
|
| 1265 |
+
|
| 1266 |
@app.callback(
|
| 1267 |
Output("model-rev", "data"),
|
| 1268 |
[Input("snd-theta-ml", "value"), Input("snd-qv-ml", "value"),
|
|
|
|
| 1390 |
]
|
| 1391 |
return html.Tbody(rows)
|
| 1392 |
|
| 1393 |
+
@app.callback(
|
| 1394 |
+
Output("skewt-graph", "figure"),
|
| 1395 |
+
Input("model-rev", "data"),
|
| 1396 |
+
)
|
| 1397 |
+
def _skewt_cb(_rev):
|
| 1398 |
+
return _skewt_figure()
|
| 1399 |
+
|
| 1400 |
@app.callback(
|
| 1401 |
[Output("buoy-profile", "figure"), Output("accel-profile", "figure")],
|
| 1402 |
Input("model-rev", "data"),
|
|
|
|
| 1446 |
.uf-center{ background: #11161f; border-radius: 8px; padding: 12px; }
|
| 1447 |
.uf-right { background: #11161f; border-radius: 8px; padding: 12px; }
|
| 1448 |
|
| 1449 |
+
.uf-section-title { font-size: 11px; font-weight: 600; color: #8d97a2; letter-spacing: 0.05em; margin-bottom: 8px; margin-top: 4px; }
|
| 1450 |
.uf-slider-row { margin-bottom: 10px; }
|
| 1451 |
.uf-slider-header { display: flex; justify-content: space-between; align-items: center; font-size: 12px; margin-bottom: 2px; }
|
| 1452 |
.uf-slider-label { color: #c7ced6; }
|
|
|
|
| 1516 |
.assume-check { color: #5ac87a; font-size: 13px; padding: 5px 10px 5px 6px; width: 18px; vertical-align: top; }
|
| 1517 |
.assume-text { color: #dfe3ea; font-weight: 600; padding: 5px 12px 5px 4px; width: 310px; vertical-align: top; }
|
| 1518 |
.assume-note { color: #8d97a2; padding: 5px 4px; line-height: 1.5; vertical-align: top; }
|
| 1519 |
+
|
| 1520 |
+
/* New 4-tab layout grids */
|
| 1521 |
+
.inp-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 16px; padding: 4px 0; }
|
| 1522 |
+
.inp-col { background: #11161f; border-radius: 8px; padding: 12px; }
|
| 1523 |
+
.skewt-layout { display: grid; grid-template-columns: 1fr 260px; gap: 16px; padding: 4px 0; }
|
| 1524 |
+
.skewt-side { background: #11161f; border-radius: 8px; padding: 12px; overflow-y: auto; }
|
| 1525 |
+
.results-layout { display: grid; grid-template-columns: 1fr 280px; gap: 16px; padding: 4px 0; }
|
| 1526 |
+
.results-center { background: #11161f; border-radius: 8px; padding: 12px; }
|
| 1527 |
+
.results-side { background: #11161f; border-radius: 8px; padding: 12px; }
|
| 1528 |
+
|
| 1529 |
+
/* Field-select dropdown — force black text inside the React-Select widget */
|
| 1530 |
+
#field-select .Select-value-label,
|
| 1531 |
+
#field-select .Select-placeholder,
|
| 1532 |
+
#field-select .Select-option,
|
| 1533 |
+
#field-select input { color: #111 !important; }
|
| 1534 |
+
#field-select .VirtualizedSelectOption { color: #111 !important; }
|
| 1535 |
"""
|
| 1536 |
|
| 1537 |
|
| 1538 |
def create_app() -> Dash:
|
| 1539 |
+
import os
|
| 1540 |
+
_assets = os.path.join(os.path.dirname(__file__), "assets")
|
| 1541 |
+
app = Dash(__name__, suppress_callback_exceptions=True, assets_folder=_assets)
|
| 1542 |
+
app.title = "Updraft Forcing: A parcel-grid model"
|
| 1543 |
app.layout = _build_layout()
|
| 1544 |
app.index_string = f"""<!doctype html><html><head>
|
| 1545 |
+
{{%metas%}}<title>{{%title%}}</title>
|
| 1546 |
+
<link rel="icon" type="image/svg+xml" href="/assets/favicon.svg">
|
| 1547 |
+
{{%css%}}
|
| 1548 |
<style>{_CSS}</style>
|
| 1549 |
</head><body>{{%app_entry%}}<footer>{{%config%}}{{%scripts%}}{{%renderer%}}</footer></body></html>"""
|
| 1550 |
_register_callbacks(app)
|
|
|
|
|
@@ -7,17 +7,17 @@ FIELD_META: dict[str, tuple[str, bool, str, str]] = {
|
|
| 7 |
"w": ("RdBu_r", True, "Vertical velocity", "m s⁻¹"),
|
| 8 |
"u": ("PuOr", False, "U-wind (zonal)", "m s⁻¹"),
|
| 9 |
"v": ("PuOr", False, "V-wind (meridional)", "m s⁻¹"),
|
| 10 |
-
"zeta": ("
|
| 11 |
-
"p_total": ("
|
| 12 |
-
"p_lin": ("
|
| 13 |
"p_spin": ("RdBu_r", True, "p' nonlinear (spin)", "Pa"),
|
| 14 |
-
"p_splat": ("
|
| 15 |
"p_buoy": ("RdBu_r", True, "p' buoyancy", "Pa"),
|
| 16 |
-
"a_lin": ("
|
| 17 |
"a_spin": ("RdBu_r", True, "Accel. spin", "m s⁻²"),
|
| 18 |
-
"a_splat": ("
|
| 19 |
"a_buoy": ("RdBu_r", True, "Accel. buoyancy", "m s⁻²"),
|
| 20 |
-
"a_total": ("
|
| 21 |
}
|
| 22 |
|
| 23 |
# Friendly display names for the dropdown
|
|
|
|
| 7 |
"w": ("RdBu_r", True, "Vertical velocity", "m s⁻¹"),
|
| 8 |
"u": ("PuOr", False, "U-wind (zonal)", "m s⁻¹"),
|
| 9 |
"v": ("PuOr", False, "V-wind (meridional)", "m s⁻¹"),
|
| 10 |
+
"zeta": ("RdBu_r", True, "Vertical vorticity ζ_z", "s⁻¹"),
|
| 11 |
+
"p_total": ("RdBu_r", True, "p' total", "Pa"),
|
| 12 |
+
"p_lin": ("RdBu_r", True, "p' linear (shear)", "Pa"),
|
| 13 |
"p_spin": ("RdBu_r", True, "p' nonlinear (spin)", "Pa"),
|
| 14 |
+
"p_splat": ("RdBu_r", True, "p' nonlinear (splat)", "Pa"),
|
| 15 |
"p_buoy": ("RdBu_r", True, "p' buoyancy", "Pa"),
|
| 16 |
+
"a_lin": ("RdBu_r", True, "Accel. linear", "m s⁻²"),
|
| 17 |
"a_spin": ("RdBu_r", True, "Accel. spin", "m s⁻²"),
|
| 18 |
+
"a_splat": ("RdBu_r", True, "Accel. splat", "m s⁻²"),
|
| 19 |
"a_buoy": ("RdBu_r", True, "Accel. buoyancy", "m s⁻²"),
|
| 20 |
+
"a_total": ("RdBu_r", True, "Total dynamic accel.", "m s⁻²"),
|
| 21 |
}
|
| 22 |
|
| 23 |
# Friendly display names for the dropdown
|