Skip to content

Commit bdc1844

Browse files
committed
tradeoff
1 parent cb4a254 commit bdc1844

8 files changed

Lines changed: 662 additions & 36 deletions

File tree

adbo/exp.py

Lines changed: 5 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@
4141
import matplotlib.pyplot as plt
4242
from adforce.wrap import idealized_tc_observe, get_default_config
4343
from adforce.constants import NEW_ORLEANS
44+
from .wrap_utils import build_wrap_config
4445
from .ani import plot_gps
4546
from .rescale import rescale_inverse
4647
from .constants import FIGURE_PATH, EXP_PATH, CONFIG_PATH, PROJECT_PATH
@@ -199,44 +200,12 @@ def obj(x: tf.Tensor) -> tf.Tensor:
199200
name: float(real_queries[i][j])
200201
for j, name in enumerate(cfg["constraints"]["order"])
201202
}
202-
wrap_cfg = get_default_config()
203-
204-
print("read config", wrap_cfg)
205203
# I want to generalize this so it's relative to the observation point not New Orleans
206204
tmp_dir = temp_dir()
207-
wrap_cfg.files.run_folder = tmp_dir
208-
# delete fort.22.nc after run
209-
wrap_cfg.files.low_storage = True
210-
if tradeoff_curve is not None:
211-
# storm on the size-intensity tradeoff curve: generate the
212-
# CLE15 profile for this sampled intensity and store it in
213-
# the run folder (self-describing provenance; fort22 accepts
214-
# a direct .json path in place of a profile name).
215-
profile_json = os.path.join(tmp_dir, "profile.json")
216-
tradeoff_curve.profile(inputs["vmax"], out_path=profile_json)
217-
wrap_cfg.tc.profile_name.value = profile_json
218-
else:
219-
wrap_cfg.tc.profile_name.value = cfg["profile_name"]
220-
wrap_cfg.adcirc.attempted_observation_location.value = [
221-
cfg["obs_lon"],
222-
cfg["obs_lat"],
223-
]
224-
wrap_cfg.adcirc["resolution"].value = cfg["resolution"]
225-
for inp in inputs:
226-
if inp == "vmax":
227-
# handled above via the tradeoff-curve profile; not a
228-
# track parameter in the adforce tc config
229-
continue
230-
if inp != "displacement":
231-
if inp == "trans_speed":
232-
wrap_cfg.tc["translation_speed"].value = inputs[inp]
233-
else:
234-
wrap_cfg.tc[inp].value = inputs[inp]
235-
if inp == "displacement":
236-
wrap_cfg.tc.impact_location.value = [
237-
cfg["obs_lon"] + inputs["displacement"],
238-
cfg["obs_lat"],
239-
]
205+
wrap_cfg = build_wrap_config(
206+
cfg, inputs, tmp_dir, tradeoff_curve=tradeoff_curve
207+
)
208+
print("read config", wrap_cfg)
240209
# if "displacement" in inputs:
241210
# # assume impact lon relative to New Orleans
242211
# inputs["impact_lon"] = NEW_ORLEANS.lon + inputs["displacement"]

adbo/exp_4d.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""adbo.exp_4d.py
2+
3+
Run a 4D Bayesian-optimization experiment varying the angle, displacement,
4+
translation speed AND intensity of the storm, with each sampled intensity
5+
placed on the size--intensity tradeoff curve r(V) of the potential size
6+
model (see ``w22.tradeoff``). Requires a precomputed curve netCDF:
7+
8+
from w22.tradeoff import env_from_point_ds, generate_curve
9+
env = env_from_point_ds("w22/data/new_orleans_august_ssp585_CESM2_r4i1p1f1_isothermal_pi4new.nc", 2025)
10+
generate_curve(env, "2025_new_orleans_r4i1p1f1")
11+
12+
Example::
13+
14+
python -m adbo.exp_4d --curve_nc w22/data/curves/2025_new_orleans_r4i1p1f1.nc \
15+
--exp_name no-4d-2025 --init_steps 35 --daf_steps 35
16+
"""
17+
18+
import argparse
19+
import os
20+
21+
import yaml
22+
23+
from adforce.constants import NEW_ORLEANS
24+
from .constants import CONFIG_PATH
25+
26+
CONSTRAINTS_4D_PATH = os.path.join(CONFIG_PATH, "4d_constraints.yaml")
27+
28+
29+
def run_4d_exp() -> None:
30+
"""Run the 4D (track + on-curve intensity) BO experiment from the CLI."""
31+
parser = argparse.ArgumentParser(description=__doc__)
32+
parser.add_argument("--curve_nc", type=str, required=True)
33+
parser.add_argument("--test", action="store_true", help="wrap_test mode")
34+
parser.add_argument("--seed", type=int, default=10)
35+
parser.add_argument("--obs_lon", type=float, default=NEW_ORLEANS.lon)
36+
parser.add_argument("--obs_lat", type=float, default=NEW_ORLEANS.lat)
37+
parser.add_argument("--init_steps", type=int, default=35)
38+
parser.add_argument("--daf_steps", type=int, default=35)
39+
parser.add_argument("--resolution", type=str, default="mid")
40+
parser.add_argument("--exp_name", type=str, default="bo-4d")
41+
parser.add_argument("--kernel", type=str, default="Matern52")
42+
parser.add_argument("--daf", type=str, default="mes")
43+
44+
# constraint overrides (vmax bounds default to the curve domain)
45+
constraints = yaml.safe_load(open(CONSTRAINTS_4D_PATH))
46+
for dim in ("angle", "trans_speed", "displacement", "vmax"):
47+
for bound in ("min", "max"):
48+
parser.add_argument(
49+
f"--{dim}_{bound}",
50+
type=float,
51+
default=constraints[dim][bound],
52+
)
53+
args = parser.parse_args()
54+
print(args)
55+
56+
for dim in ("angle", "trans_speed", "displacement", "vmax"):
57+
constraints[dim]["min"] = getattr(args, f"{dim}_min")
58+
constraints[dim]["max"] = getattr(args, f"{dim}_max")
59+
60+
# heavy import (tensorflow/trieste) deferred until actually running
61+
from .exp import run_bayesopt_exp
62+
63+
run_bayesopt_exp(
64+
constraints=constraints,
65+
seed=args.seed,
66+
exp_name=args.exp_name,
67+
resolution=args.resolution,
68+
obs_lon=args.obs_lon,
69+
obs_lat=args.obs_lat,
70+
init_steps=args.init_steps,
71+
daf_steps=args.daf_steps,
72+
daf=args.daf,
73+
kernel=args.kernel,
74+
wrap_test=args.test,
75+
curve_path=args.curve_nc,
76+
)
77+
78+
79+
if __name__ == "__main__":
80+
run_4d_exp()

adbo/plot_tradeoff.py

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
"""Plots for the size--intensity tradeoff experiments.
2+
3+
- :func:`plot_vmax_sweep`: surge height against intensity from a 1D sweep
4+
(``adbo.sweep_vmax``), with the curve's r0/rmax context panels.
5+
- :func:`plot_4d_samples`: surge against sampled intensity from a 4D BO
6+
experiment (``adbo.exp_4d``), split into initial-design vs acquisition
7+
samples.
8+
9+
Both read the ``experiments.json`` ledger, so they work on partial runs.
10+
11+
Example::
12+
13+
python -m adbo.plot_tradeoff --exp_dir <exp>/no-sweep-2025 --mode sweep
14+
"""
15+
16+
import argparse
17+
import json
18+
import os
19+
from typing import Optional
20+
21+
import matplotlib
22+
23+
matplotlib.use("Agg")
24+
import matplotlib.pyplot as plt
25+
import numpy as np
26+
27+
28+
def _load_ledger(exp_dir: str) -> dict:
29+
with open(os.path.join(exp_dir, "experiments.json"), "r", encoding="utf-8") as f:
30+
return json.load(f)
31+
32+
33+
def _fig_path(exp_dir: str, name: str) -> str:
34+
img_dir = os.path.join(exp_dir, "img")
35+
os.makedirs(img_dir, exist_ok=True)
36+
return os.path.join(img_dir, name)
37+
38+
39+
def _curve_context(curve_nc: Optional[str]):
40+
if curve_nc is None or not os.path.exists(curve_nc):
41+
return None
42+
from w22.tradeoff import TradeoffCurve
43+
44+
return TradeoffCurve.from_file(curve_nc)
45+
46+
47+
def plot_vmax_sweep(exp_dir: str, curve_nc: Optional[str] = None) -> str:
48+
"""Plot surge height vs intensity for a 1D sweep experiment.
49+
50+
Args:
51+
exp_dir (str): Sweep experiment directory (contains experiments.json).
52+
curve_nc (str, optional): Tradeoff curve for context panels; defaults
53+
to the path recorded in sweep-config.json.
54+
55+
Returns:
56+
str: Path of the written figure.
57+
"""
58+
ledger = _load_ledger(exp_dir)
59+
if curve_nc is None:
60+
cfg_path = os.path.join(exp_dir, "sweep-config.json")
61+
if os.path.exists(cfg_path):
62+
with open(cfg_path, "r", encoding="utf-8") as f:
63+
curve_nc = json.load(f).get("curve_path")
64+
curve = _curve_context(curve_nc)
65+
66+
v = np.array([rec["vmax"] for rec in ledger.values()])
67+
z = np.array([rec["res"] for rec in ledger.values()])
68+
order = np.argsort(v)
69+
v, z = v[order], z[order]
70+
ok = np.isfinite(z)
71+
72+
nrows = 2 if curve is not None else 1
73+
fig, axs = plt.subplots(
74+
nrows, 1, sharex=True, figsize=(6, 3 * nrows), squeeze=False
75+
)
76+
ax = axs[0][0]
77+
ax.plot(v[ok], z[ok], "o-", color="tab:blue")
78+
if (~ok).any():
79+
for vf in v[~ok]:
80+
ax.axvline(vf, color="red", alpha=0.3, linestyle=":")
81+
if len(z[ok]):
82+
i_best = int(np.nanargmax(z[ok]))
83+
ax.plot(v[ok][i_best], z[ok][i_best], "*", color="tab:orange", markersize=15)
84+
ax.annotate(
85+
f"max {z[ok][i_best]:.2f} m @ {v[ok][i_best]:.1f} m/s",
86+
(v[ok][i_best], z[ok][i_best]),
87+
textcoords="offset points",
88+
xytext=(5, -12),
89+
)
90+
ax.set_ylabel("Max SSH at observation point [m]")
91+
ax.set_title("Surge along the size-intensity tradeoff curve")
92+
93+
if curve is not None:
94+
ax.axvline(curve.v_max, color="green", linestyle="--", alpha=0.7)
95+
ax.annotate(
96+
"$V_p$", (curve.v_max, ax.get_ylim()[0]), color="green", ha="right"
97+
)
98+
ax2 = axs[1][0]
99+
vv = np.linspace(curve.v_min, curve.v_max, 200)
100+
ax2.plot(vv, [curve.rmax(x) / 1000 for x in vv], color="tab:purple")
101+
ax2.set_ylabel("$r_{\\mathrm{max}}(V)$ [km]", color="tab:purple")
102+
ax2b = ax2.twinx()
103+
ax2b.plot(vv, [curve.r0(x) / 1000 for x in vv], color="tab:gray")
104+
ax2b.set_ylabel("$r_0(V)$ [km]", color="tab:gray")
105+
ax2.set_xlabel("Maximum gradient wind speed $V$ [m s$^{-1}$]")
106+
else:
107+
ax.set_xlabel("Maximum gradient wind speed $V$ [m s$^{-1}$]")
108+
109+
out = _fig_path(exp_dir, "vmax_sweep.pdf")
110+
plt.tight_layout()
111+
plt.savefig(out)
112+
plt.close(fig)
113+
print(f"plot_vmax_sweep: wrote {out}")
114+
return out
115+
116+
117+
def plot_4d_samples(
118+
exp_dir: str, init_steps: Optional[int] = None, curve_nc: Optional[str] = None
119+
) -> str:
120+
"""Plot surge vs sampled intensity for a 4D BO experiment.
121+
122+
Args:
123+
exp_dir (str): BO experiment directory.
124+
init_steps (int, optional): Number of initial-design samples; defaults
125+
to the value in bo-config.json.
126+
curve_nc (str, optional): Curve for the V_p marker; defaults to the
127+
path in bo-config.json.
128+
129+
Returns:
130+
str: Path of the written figure.
131+
"""
132+
ledger = _load_ledger(exp_dir)
133+
bo_cfg_path = os.path.join(exp_dir, "bo-config.json")
134+
if os.path.exists(bo_cfg_path):
135+
with open(bo_cfg_path, "r", encoding="utf-8") as f:
136+
bo_cfg = json.load(f)
137+
init_steps = init_steps if init_steps is not None else bo_cfg.get("init_steps")
138+
curve_nc = curve_nc if curve_nc is not None else bo_cfg.get("curve_path")
139+
curve = _curve_context(curve_nc)
140+
141+
calls = np.array(sorted(int(k) for k in ledger))
142+
v = np.array([ledger[str(c)]["vmax"] for c in calls])
143+
z = np.array([ledger[str(c)]["res"] for c in calls])
144+
is_init = (
145+
calls < init_steps if init_steps is not None else np.ones_like(calls, bool)
146+
)
147+
148+
fig, ax = plt.subplots(figsize=(6, 4))
149+
ax.scatter(
150+
v[is_init], z[is_init], marker="x", color="tab:blue", label="initial design"
151+
)
152+
if (~is_init).any():
153+
sc = ax.scatter(
154+
v[~is_init],
155+
z[~is_init],
156+
c=calls[~is_init],
157+
marker="o",
158+
cmap="viridis",
159+
label="acquisition",
160+
)
161+
plt.colorbar(sc, ax=ax, label="evaluation index")
162+
if len(z):
163+
i_best = int(np.nanargmax(z))
164+
ax.plot(v[i_best], z[i_best], "*", color="tab:orange", markersize=15)
165+
if curve is not None:
166+
ax.axvline(curve.v_max, color="green", linestyle="--", alpha=0.7)
167+
ax.annotate(
168+
"$V_p$", (curve.v_max, ax.get_ylim()[0]), color="green", ha="right"
169+
)
170+
ax.set_xlabel("Maximum gradient wind speed $V$ [m s$^{-1}$]")
171+
ax.set_ylabel("Max SSH at observation point [m]")
172+
ax.legend()
173+
174+
out = _fig_path(exp_dir, "bo_4d_samples.pdf")
175+
plt.tight_layout()
176+
plt.savefig(out)
177+
plt.close(fig)
178+
print(f"plot_4d_samples: wrote {out}")
179+
return out
180+
181+
182+
if __name__ == "__main__":
183+
parser = argparse.ArgumentParser(description=__doc__)
184+
parser.add_argument("--exp_dir", type=str, required=True)
185+
parser.add_argument("--mode", type=str, choices=["sweep", "4d"], default="sweep")
186+
parser.add_argument("--curve_nc", type=str, default=None)
187+
args = parser.parse_args()
188+
if args.mode == "sweep":
189+
plot_vmax_sweep(args.exp_dir, curve_nc=args.curve_nc)
190+
else:
191+
plot_4d_samples(args.exp_dir, curve_nc=args.curve_nc)

0 commit comments

Comments
 (0)