# Copyright 2013 – present by the SalishSeaCast Project contributors
# and The University of British Columbia
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
# SPDX-License-Identifier: Apache-2.0
"""SalishSeaCast worker that produces visualization images for the website from run results."""
import logging
import os
import shlex
import subprocess
from glob import glob
from pathlib import Path
# **IMPORTANT**: matplotlib must be imported before anything else that uses it
# because of the matplotlib.use() call below
import matplotlib
## TODO: Get rid of matplotlib.use() call; see issue #19
matplotlib.use("Agg")
import matplotlib.pyplot
import arrow
import cmocean
from nemo_nowcast import NowcastWorker
import netCDF4 as nc
import scipy.io as sio
import xarray
from nowcast import lib
from nowcast.figures.research import (
baynes_sound_agrif,
time_series_plots,
tracer_thalweg_and_surface_hourly,
velocity_section_and_surface,
)
from nowcast.figures.comparison import compare_venus_ctd, sandheads_winds
from nowcast.figures.publish import (
pt_atkinson_tide,
storm_surge_alerts,
storm_surge_alerts_thumbnail,
compare_tide_prediction_max_ssh,
)
from nowcast.figures.wwatch3 import wave_height_period
# Legacy figures code
from nowcast.figures import research_VENUS
NAME = "make_plots"
logger = logging.getLogger(NAME)
[docs]
def main():
"""For command-line usage see:
:command:`python -m nowcast.workers.make_plots --help`
"""
worker = NowcastWorker(NAME, description=__doc__)
worker.init_cli()
worker.cli.add_argument(
"model",
choices={"nemo", "wwatch3"},
help="""
Model to produce plots for:
'nemo' means the SalishSeaCast NEMO model,
'wwatch3' means the Strait of Georgia WaveWatch3(TM) model,
""",
)
worker.cli.add_argument(
"run_type",
choices={
"nowcast",
"nowcast-green",
"nowcast-agrif",
"forecast",
"forecast2",
},
help="""
Type of run to produce plots for:
'nowcast' means NEMO nowcast physics-only run, or wwatch3 nowcast run,
'nowcast-green' means NEMO nowcast-green physics/biology run,
'nowcast-agrif' means NEMO nowcast-green physics/biology runs with AGRIF sub-grid(s),
'forecast' means NEMO forecast physics-only runs, or wwatch3 forecast run,
'forecast2' means NEMO preliminary forecast physics-only runs, or wwatch3 preliminary
forecast run,
""",
)
worker.cli.add_argument(
"plot_type",
choices={"publish", "research", "comparison"},
help="""
Which type of plots to produce:
"publish" means storm surge and other approved plots for publishing
(forecast and forecast2 runs only),
"research" means tracers, currents and other research plots
(nowcast and nowcast-green runs only)
"comparison" means model vs. observation plots (nowcast runs only)
""",
)
worker.cli.add_date_option(
"--run-date",
default=arrow.now().floor("day"),
help="Date of the run to make plots for.",
)
worker.cli.add_argument(
"--test-figure",
help="""Identifier for a single figure to do a test on.
The identifier may be the svg_name of the figure used in make_plots
(e.g. SH_wind is the svg_name of figures stored as SH_wind_{ddmmmyy}.svg),
the name of the website figure module
(e.g. storm_surge_alerts is the module name of
nowcast.figures.publish.storm_surge_alerts).
The figure will be rendered in
/results/nowcast-sys/figures/test/{run_type}/{ddmmmyy}/ so that it is
accessible in a browser at
https://salishsea.eos.ubc.ca/{run_type}/{ddmmmyy}/{svg_name}_{ddmmyy}.svg
""",
)
worker.run(make_plots, success, failure)
return worker
def success(parsed_args):
logger.info(
f"{parsed_args.model} {parsed_args.plot_type} plots for "
f'{parsed_args.run_date.format("YYYY-MM-DD")} '
f"{parsed_args.run_type} completed"
)
msg_type = (
f"success {parsed_args.model} {parsed_args.run_type} "
f"{parsed_args.plot_type}"
)
return msg_type
def failure(parsed_args):
logger.critical(
f"{parsed_args.model} {parsed_args.plot_type} plots failed for "
f'{parsed_args.run_date.format("YYYY-MM-DD")} {parsed_args.run_type}'
)
msg_type = (
f"failure {parsed_args.model} {parsed_args.run_type} "
f"{parsed_args.plot_type}"
)
return msg_type
def make_plots(parsed_args, config, *args):
model = parsed_args.model
run_date = parsed_args.run_date
dmy = run_date.format("DDMMMYY").lower()
timezone = config["figures"]["timezone"]
run_type = parsed_args.run_type
plot_type = parsed_args.plot_type
test_figure_id = parsed_args.test_figure
fig_functions = {}
if model == "nemo":
dev_results_home = (
None
if config["results archive"]["nowcast-dev"] == "None"
else Path(config["results archive"]["nowcast-dev"])
)
weather_path = Path(config["weather"]["ops dir"])
if run_type in ["forecast", "forecast2"]:
weather_path = weather_path / "fcst"
results_dir = Path(config["results archive"][run_type], dmy)
grid_dir = Path(config["figures"]["grid dir"])
bathy = nc.Dataset(grid_dir / config["run types"][run_type]["bathymetry"])
mesh_mask = nc.Dataset(grid_dir / config["run types"][run_type]["mesh mask"])
dev_mesh_mask = nc.Dataset(
grid_dir / config["run types"]["nowcast-dev"]["mesh mask"]
)
coastline = sio.loadmat(config["figures"]["coastline"])
if run_type == "nowcast" and plot_type == "research":
fig_functions = _prep_nowcast_research_fig_functions(
bathy, mesh_mask, results_dir, run_date
)
if run_type == "nowcast-green" and plot_type == "research":
fig_functions = _prep_nowcast_green_research_fig_functions(
config, bathy, mesh_mask, results_dir, run_date
)
if run_type == "nowcast-agrif" and plot_type == "research":
fig_functions = _prep_nowcast_agrif_research_fig_functions(
config, results_dir, run_date
)
if run_type == "nowcast" and plot_type == "comparison":
fig_functions = _prep_comparison_fig_functions(
config,
bathy,
coastline,
mesh_mask,
dev_mesh_mask,
results_dir,
run_type,
run_date,
dev_results_home,
dmy,
timezone,
)
if run_type.startswith("forecast") and plot_type == "publish":
fig_functions = _prep_publish_fig_functions(
config,
bathy,
coastline,
weather_path,
results_dir,
run_type,
run_date,
timezone,
)
if model == "wwatch3":
wwatch3_dataset_url = config["figures"]["dataset URLs"]["wwatch3 fields"]
fig_functions = _prep_wwatch3_publish_fig_functions(
wwatch3_dataset_url, run_type, run_date
)
checklist = _render_figures(
config, model, run_type, plot_type, dmy, fig_functions, test_figure_id
)
return checklist
def _results_dataset(period, grid, results_dir):
"""Return the results dataset for period (e.g. 1h or 1d)
and grid (e.g. grid_T, grid_U) from results_dir.
"""
filename_pattern = "SalishSea_{period}_*_{grid}.nc"
filepaths = glob(
os.path.join(results_dir, filename_pattern.format(period=period, grid=grid))
)
return nc.Dataset(filepaths[0])
def _results_dataset_gridded(station, results_dir):
"""Return the results dataset for station (e.g. central or east)
for the quarter hourly data from results_dir.
"""
filename_pattern = "VENUS_{station}_gridded.nc"
filepaths = glob(
os.path.join(results_dir, filename_pattern.format(station=station))
)
return nc.Dataset(filepaths[0])
def _prep_nowcast_research_fig_functions(bathy, mesh_mask, results_dir, run_date):
logger.info(
f"preparing render list for {run_date.format('YYYY-MM-DD')} NEMO nowcast-blue research figures"
)
yyyymmdd = run_date.format("YYYYMMDD")
grid_T_hr = _results_dataset("1h", "grid_T", results_dir)
grid_U_hr = _results_dataset("1h", "grid_U", results_dir)
grid_V_hr = _results_dataset("1h", "grid_V", results_dir)
grid_central = _results_dataset_gridded("central", results_dir)
grid_east = _results_dataset_gridded("east", results_dir)
image_loops = {
"salinity": {"nemo var": "vosaline", "cmap": cmocean.cm.haline},
"temperature": {"nemo var": "votemper", "cmap": cmocean.cm.thermal},
}
fig_functions = {}
for tracer, params in image_loops.items():
clevels_thalweg, clevels_surface = tracer_thalweg_and_surface_hourly.clevels(
grid_T_hr.variables[params["nemo var"]], mesh_mask, depth_integrated=False
)
fig_functions.update(
{
f"{tracer}_thalweg_and_surface_{yyyymmdd}_{hr:02d}3000_UTC": {
"function": tracer_thalweg_and_surface_hourly.make_figure,
"args": (
hr,
grid_T_hr.variables[params["nemo var"]],
bathy,
mesh_mask,
clevels_thalweg,
clevels_surface,
),
"kwargs": {"cmap": params["cmap"], "depth_integrated": False},
"format": "png",
"image loop": True,
}
for hr in range(24)
}
)
logger.info(
f"added {tracer}_thalweg_and_surface figures to {run_date.format('YYYY-MM-DD')} "
f"NEMO nowcast-blue research render list"
)
fig_functions.update(
{
"Currents_sections_and_surface": {
"function": velocity_section_and_surface.make_figure,
"args": (
grid_U_hr.variables["vozocrtx"],
grid_V_hr.variables["vomecrty"],
bathy,
mesh_mask,
),
"kwargs": {
"sections": (450, 520, 680),
"pos": ((0.1, 0.35), (0.4, 0.65), (0.7, 0.95)),
"section_lims": (
(235, 318, 0, 445),
(192, 277, 0, 445),
(127, 197, 0, 445),
),
},
},
"Currents_at_VENUS_Central": {
"function": research_VENUS.plot_vel_NE_gridded,
"args": ("Central", grid_central),
},
"Currents_at_VENUS_East": {
"function": research_VENUS.plot_vel_NE_gridded,
"args": ("East", grid_east),
},
}
)
for fig_func in fig_functions:
if fig_func.startswith("Currents"):
logger.info(
f"added {fig_func} figure to {run_date.format('YYYY-MM-DD')} NEMO nowcast-blue "
f"research render list"
)
return fig_functions
def _prep_nowcast_green_research_fig_functions(
config, bathy, mesh_mask, results_dir, run_date
):
logger.info(
f"preparing render list for {run_date.format('YYYY-MM-DD')} NEMO nowcast-green research figures"
)
yyyymmdd = run_date.format("YYYYMMDD")
grid_T_hr = _results_dataset("1h", "grid_T", results_dir)
biol_T_hr = _results_dataset("1h", "biol_T", results_dir)
turb_T_hr = _results_dataset("1h", "chem_T", results_dir)
fig_functions = {}
image_loops = {
"salinity": {"nemo var": "vosaline", "cmap": cmocean.cm.haline},
"temperature": {"nemo var": "votemper", "cmap": cmocean.cm.thermal},
}
for tracer, params in image_loops.items():
clevels_thalweg, clevels_surface = tracer_thalweg_and_surface_hourly.clevels(
grid_T_hr.variables[params["nemo var"]], mesh_mask, depth_integrated=False
)
fig_functions.update(
{
f"{tracer}_thalweg_and_surface_{yyyymmdd}_{hr:02d}3000_UTC": {
"function": tracer_thalweg_and_surface_hourly.make_figure,
"args": (
hr,
grid_T_hr.variables[params["nemo var"]],
bathy,
mesh_mask,
clevels_thalweg,
clevels_surface,
),
"kwargs": {"cmap": params["cmap"], "depth_integrated": False},
"format": "png",
"image loop": True,
}
for hr in range(24)
}
)
logger.info(
f"added {tracer}_thalweg_and_surface figures to {run_date.format('YYYY-MM-DD')} NEMO "
f"nowcast-green research render list"
)
image_loops = {
"nitrate": {
"nemo var": "nitrate",
"cmap": cmocean.cm.tempo,
"depth integrated": False,
},
"ammonium": {
"nemo var": "ammonium",
"cmap": cmocean.cm.matter,
"depth integrated": False,
},
"silicon": {
"nemo var": "silicon",
"cmap": cmocean.cm.turbid,
"depth integrated": False,
},
"dissolved_organic_nitrogen": {
"nemo var": "dissolved_organic_nitrogen",
"cmap": cmocean.cm.amp,
"depth integrated": False,
},
"particulate_organic_nitrogen": {
"nemo var": "particulate_organic_nitrogen",
"cmap": cmocean.cm.amp,
"depth integrated": False,
},
"biogenic_silicon": {
"nemo var": "biogenic_silicon",
"cmap": cmocean.cm.turbid,
"depth integrated": False,
},
"diatoms": {
"nemo var": "diatoms",
"cmap": cmocean.cm.algae,
"depth integrated": True,
},
"flagellates": {
"nemo var": "flagellates",
"cmap": cmocean.cm.algae,
"depth integrated": True,
},
"microzooplankton": {
"nemo var": "microzooplankton",
"cmap": cmocean.cm.algae,
"depth integrated": True,
},
"mesozooplankton": {
"nemo var": "mesozooplankton",
"cmap": cmocean.cm.algae,
"depth integrated": True,
},
}
for tracer, params in image_loops.items():
clevels_thalweg, clevels_surface = tracer_thalweg_and_surface_hourly.clevels(
biol_T_hr.variables[params["nemo var"]],
mesh_mask,
depth_integrated=params["depth integrated"],
)
fig_functions.update(
{
f"{tracer}_thalweg_and_surface_{yyyymmdd}_{hr:02d}3000_UTC": {
"function": tracer_thalweg_and_surface_hourly.make_figure,
"args": (
hr,
biol_T_hr.variables[params["nemo var"]],
bathy,
mesh_mask,
clevels_thalweg,
clevels_surface,
),
"kwargs": {
"cmap": params["cmap"],
"depth_integrated": params["depth integrated"],
},
"format": "png",
"image loop": True,
}
for hr in range(24)
}
)
logger.info(
f"added {tracer}_thalweg_and_surface figures to {run_date.format('YYYY-MM-DD')} NEMO "
f"nowcast-green research render list"
)
image_loops = {
"turbidity": {
"nemo var": "turbidity",
"cmap": cmocean.cm.turbid,
"depth integrated": False,
}
}
for tracer, params in image_loops.items():
clevels_thalweg, clevels_surface = tracer_thalweg_and_surface_hourly.clevels(
turb_T_hr.variables[params["nemo var"]],
mesh_mask,
depth_integrated=params["depth integrated"],
)
fig_functions.update(
{
f"{tracer}_thalweg_and_surface_{yyyymmdd}_{hr:02d}3000_UTC": {
"function": tracer_thalweg_and_surface_hourly.make_figure,
"args": (
hr,
turb_T_hr.variables[params["nemo var"]],
bathy,
mesh_mask,
clevels_thalweg,
clevels_surface,
),
"kwargs": {
"cmap": params["cmap"],
"depth_integrated": params["depth integrated"],
},
"format": "png",
"image loop": True,
}
for hr in range(24)
}
)
logger.info(
f"added {tracer}_thalweg_and_surface figures to {run_date.format('YYYY-MM-DD')} NEMO "
f"nowcast-green research render list"
)
place = "S3"
phys_dataset = xarray.open_dataset(
config["figures"]["dataset URLs"]["3d physics fields"]
)
bio_dataset = xarray.open_dataset(
config["figures"]["dataset URLs"]["3d biology fields"]
)
fig_functions.update(
{
"temperature_salinity_timeseries": {
"function": time_series_plots.make_figure,
"args": (phys_dataset, "temperature", "salinity", place),
},
"nitrate_diatoms_timeseries": {
"function": time_series_plots.make_figure,
"args": (bio_dataset, "nitrate", "diatoms", place),
},
"diatoms_flagellates_timeseries": {
"function": time_series_plots.make_figure,
"args": (bio_dataset, "diatoms", "flagellates", place),
},
"z1_z2_zooplankton_timeseries": {
"function": time_series_plots.make_figure,
"args": (bio_dataset, "z1_zooplankton", "z2_zooplankton", place),
},
}
)
for fig_func in fig_functions:
if fig_func.endswith("timeseries"):
logger.info(
f"added {fig_func} figure to {run_date.format('YYYY-MM-DD')} NEMO "
f"nowcast-green research render list"
)
return fig_functions
def _prep_nowcast_agrif_research_fig_functions(config, agrif_results_dir, run_date):
logger.info(
f"preparing render list for {run_date.format('YYYY-MM-DD')} NEMO nowcast-agrif research figures"
)
yyyymmdd = run_date.format("YYYYMMDD")
ss_tracers_path = agrif_results_dir / "BaynesSoundSurface_grid_T.nc"
bs_phys_path = agrif_results_dir / f"1_SalishSea_1h_{yyyymmdd}_{yyyymmdd}_grid_T.nc"
bs_bio_path = agrif_results_dir / f"1_SalishSea_1h_{yyyymmdd}_{yyyymmdd}_ptrc_T.nc"
grid_dir = Path(config["figures"]["grid dir"])
ss_grid_path = grid_dir / config["run types"]["nowcast-agrif"]["bathymetry"]
bs_grid_path = Path(config["run types"]["nowcast-agrif"]["sub-grid bathymetry"])
fig_functions = {
"baynes_sound_surface": {
"function": baynes_sound_agrif.make_figure,
"args": (
ss_tracers_path,
bs_phys_path,
bs_bio_path,
run_date,
ss_grid_path,
bs_grid_path,
),
}
}
logger.info(
f"added baynes_sound_surface figure to {run_date.format('YYYY-MM-DD')} NEMO "
f"nowcast-agrif research render list"
)
return fig_functions
def _prep_comparison_fig_functions(
config,
bathy,
coastline,
mesh_mask,
dev_mesh_mask,
results_dir,
run_type,
run_date,
dev_results_home,
dmy,
timezone,
):
logger.info(
f"preparing render list for {run_date.format('YYYY-MM-DD')} NEMO nowcast-blue comparison figures"
)
hrdps_dataset_url = config["figures"]["dataset URLs"]["HRDPS fields"]
if dev_results_home is None:
dev_grid_T_hr = None
else:
dev_grid_T_hr = _results_dataset("1h", "grid_T", dev_results_home / dmy)
grid_T_hr = _results_dataset("1h", "grid_T", results_dir)
fig_functions = {
"SH_wind": {
"function": sandheads_winds.make_figure,
"args": (hrdps_dataset_url, run_type, run_date, coastline),
},
"Compare_VENUS_East": {
"function": compare_venus_ctd.make_figure,
"args": (
"East node",
grid_T_hr,
dev_grid_T_hr,
timezone,
mesh_mask,
dev_mesh_mask,
),
},
"Compare_VENUS_Central": {
"function": compare_venus_ctd.make_figure,
"args": (
"Central node",
grid_T_hr,
dev_grid_T_hr,
timezone,
mesh_mask,
dev_mesh_mask,
),
},
}
for fig_func in fig_functions:
logger.info(
f"added {fig_func} figure to {run_date.format('YYYY-MM-DD')} NEMO nowcast-blue "
f"comparison render list"
)
return fig_functions
def _prep_publish_fig_functions(
config, bathy, coastline, weather_path, results_dir, run_type, run_date, timezone
):
logger.info(
f"preparing render list for {run_date.format('YYYY-MM-DD')} NEMO {run_type} publish figures"
)
ssh_fcst_dataset_url_tmpl = config["figures"]["dataset URLs"][
"tide stn ssh time series"
]
tidal_predictions = Path(config["ssh"]["tidal predictions"])
forecast_hrs = int(config["run types"][run_type]["duration"] * 24)
grid_T_hr = _results_dataset("1h", "grid_T", results_dir)
start_day = {
"forecast": run_date.shift(days=+1).format("YYYYMMDD"),
"forecast2": run_date.shift(days=+2).format("YYYYMMDD"),
}
end_day = {
"forecast": run_date.shift(days=+2).format("YYYYMMDD"),
"forecast2": run_date.shift(days=+3).format("YYYYMMDD"),
}
grid_T_hr_path = (
results_dir
/ f"SalishSea_1h_{start_day[run_type]}_{end_day[run_type]}_grid_T.nc"
)
names = {
"Boundary Bay": "BB_maxSSH",
"Campbell River": "CR_maxSSH",
"Cherry Point": "CP_maxSSH",
"Friday Harbor": "FH_maxSSH",
"Halfmoon Bay": "HB_maxSSH",
"Nanaimo": "Nan_maxSSH",
"Neah Bay": "NB_maxSSH",
"New Westminster": "NW_maxSSH",
"Patricia Bay": "PB_maxSSH",
"Point Atkinson": "PA_maxSSH",
"Port Renfrew": "PR_maxSSH",
"Sand Heads": "SH_maxSSH",
"Sandy Cove": "SC_maxSSH",
"Squamish": "Sqam_maxSSH",
"Victoria": "Vic_maxSSH",
"Woodwards Landing": "WL_maxSSH",
}
grids_10m = {
name: nc.Dataset(results_dir / "{}.nc".format(name.replace(" ", "")))
for name in names
}
fig_functions = {
"Website_thumbnail": {
"function": storm_surge_alerts_thumbnail.make_figure,
"args": (grids_10m, weather_path, coastline, tidal_predictions),
"format": "png",
},
"Threshold_website": {
"function": storm_surge_alerts.make_figure,
"args": (grids_10m, weather_path, coastline, tidal_predictions),
},
"PA_tidal_predictions": {
"function": pt_atkinson_tide.make_figure,
"args": (grid_T_hr, tidal_predictions, timezone),
},
}
for fig_func in fig_functions:
logger.info(
f"added {fig_func} figure to {run_date.format('YYYY-MM-DD')} NEMO {run_type} publish render list"
)
for place, svg_root in names.items():
fig_functions.update(
{
svg_root: {
"function": compare_tide_prediction_max_ssh.make_figure,
"args": (
place,
ssh_fcst_dataset_url_tmpl,
tidal_predictions,
forecast_hrs,
weather_path,
bathy,
grid_T_hr_path,
),
}
}
)
logger.info(
f"added {place} figure to {run_date.format('YYYY-MM-DD')} NEMO {run_type} publish render list"
)
return fig_functions
def _prep_wwatch3_publish_fig_functions(wwatch3_dataset_url, run_type, run_date):
logger.info(
f"preparing render list for {run_date.format('YYYY-MM-DD')} WaveWatch3 {run_type} publish figures"
)
buoys = {"Halibut Bank": "HB_waves", "Sentry Shoal": "SS_waves"}
fig_functions = {}
for buoy, svg_root in buoys.items():
fig_functions.update(
{
svg_root: {
"function": wave_height_period.make_figure,
"args": (buoy, wwatch3_dataset_url),
}
}
)
logger.info(
f"added {buoy} figure to {run_date.format('YYYY-MM-DD')} WaveWatch3 {run_type} "
f"publish render list"
)
return fig_functions
def _render_figures(
config, model, run_type, plot_type, dmy, fig_functions, test_figure_id
):
logger.info(f"starting to render {model} {run_type} {plot_type} {dmy} figures")
checklist = {}
fig_files = []
for svg_name, func in fig_functions.items():
fig_func = func["function"]
args = func.get("args", [])
kwargs = func.get("kwargs", {})
fig_save_format = func.get("format", "svg")
image_loop_figure = func.get("image loop", False)
test_figure = False
if test_figure_id:
test_figure = any(
(
svg_name == test_figure_id,
image_loop_figure and svg_name.startswith(test_figure_id),
fig_func.__module__.endswith(f"{plot_type}.{test_figure_id}"),
)
)
if not test_figure:
continue
logger.debug(f"starting {fig_func.__module__}.{fig_func.__name__}")
try:
fig = _calc_figure(fig_func, args, kwargs)
except FileNotFoundError, IndexError, KeyError, TypeError:
# **IMPORTANT**: the collection of exceptions above must match those
# handled in the _calc_figure() function
continue
if test_figure:
fig_files_dir = Path(config["figures"]["test path"], run_type, dmy)
fig_files_dir.mkdir(parents=True, exist_ok=True)
else:
fig_files_dir = (
Path(config["figures"]["storage path"], run_type, dmy)
if model == "nemo"
else Path(config["figures"]["storage path"], model, run_type, dmy)
)
lib.mkdir(fig_files_dir, logger, grp_name=config["file group"])
filename = fig_files_dir / f"{svg_name}_{dmy}.{fig_save_format}"
if image_loop_figure:
filename = fig_files_dir / f"{svg_name}.{fig_save_format}"
fig.savefig(
os.fspath(filename), facecolor=fig.get_facecolor(), bbox_inches="tight"
)
logger.debug(f"{filename} saved")
matplotlib.pyplot.close(fig)
if fig_save_format == "svg":
logger.debug(f"starting SVG scouring of {filename}")
tmpfilename = filename.with_suffix(".scour")
scour = Path(os.environ["NOWCAST_ENV"], "bin", "scour")
cmd = f"{scour} {filename} {tmpfilename}"
logger.debug(f"running subprocess: {cmd}")
try:
proc = subprocess.run(
shlex.split(cmd),
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
universal_newlines=True,
)
except subprocess.CalledProcessError as e:
logger.warning("SVG scouring failed, proceeding with unscoured figure")
logger.debug(f"scour return code: {e.returncode}")
if e.output:
logger.debug(e.output)
continue
logger.debug(proc.stdout)
tmpfilename.rename(filename)
logger.debug(f"{filename} scoured")
lib.fix_perms(filename, grp_name=config["file group"])
fig_files.append(os.fspath(filename))
fig_path = _render_storm_surge_alerts_thumbnail(
config,
run_type,
plot_type,
dmy,
fig,
svg_name,
fig_save_format,
test_figure,
)
if checklist is not None:
checklist["storm surge alerts thumbnail"] = fig_path
checklist[f"{model} {run_type} {plot_type}"] = fig_files
logger.info(f"finished rendering {model} {run_type} {plot_type} {dmy} figures")
return checklist
def _calc_figure(fig_func, args, kwargs):
try:
fig = fig_func(*args, **kwargs)
except FileNotFoundError as e:
if fig_func.__name__.endswith("salinity_ferry_route"):
logger.warning(
f"{args[3]} ferry route salinity comparison figure failed: {e}"
)
else:
logger.error(
f"unexpected FileNotFoundError in {fig_func.__name__}:", exc_info=True
)
raise
except KeyError:
if fig_func.__name__.endswith("salinity_ferry_route"):
logger.warning(
f"{args[3]} ferry route salinity comparison figure "
f"failed: No observations found in .mat file"
)
else:
logger.error(f"unexpected KeyError in {fig_func.__name__}:", exc_info=True)
raise
except TypeError:
if fig_func.__module__.endswith("compare_venus_ctd"):
logger.warning(
f"VENUS {args[0]} CTD comparison figure failed: "
f"No observations available"
)
else:
logger.error(f"unexpected TypeError in {fig_func.__name__}:", exc_info=True)
raise
return fig
def _render_storm_surge_alerts_thumbnail(
config, run_type, plot_type, dmy, fig, svg_name, fig_save_format, test_figure
):
"""Undated storm surge alerts thumbnail for storm-surge/index.html page"""
now = arrow.now()
today_dmy = now.format("DDMMMYY").lower()
yesterday_dmy = now.shift(days=-1).format("DDMMMYY").lower()
thumbnail_root = config["figures"]["storm surge alerts thumbnail"]
if not all(
(
plot_type == "publish",
svg_name == thumbnail_root,
any(
(
run_type == "forecast" and dmy == today_dmy,
run_type == "forecast2" and dmy == yesterday_dmy,
)
),
)
):
return
if test_figure:
dest_dir = Path(
config["figures"]["test path"],
config["figures"]["storm surge info portal path"],
)
dest_dir.mkdir(parents=True, exist_ok=True)
else:
dest_dir = Path(
config["figures"]["storage path"],
config["figures"]["storm surge info portal path"],
)
undated_thumbnail = dest_dir / f"{thumbnail_root}.{fig_save_format}"
fig.savefig(
os.fspath(undated_thumbnail), facecolor=fig.get_facecolor(), bbox_inches="tight"
)
lib.fix_perms(undated_thumbnail, grp_name=config["file group"])
logger.debug(f"{undated_thumbnail} saved")
return os.fspath(undated_thumbnail)
if __name__ == "__main__":
main()