Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -467,9 +467,11 @@
// Test --update_ssds_provenance with a short log_file
//"args": ["-v", "1", "--log_file", "ahi/missionlogs/2025/20250128_20250131/20250131T051404/202501310514_202501310535.nc4", "--update_ssds_provenance"]
// Test ahi mission that has Backseat Planktivore data with --update_ssds_provenance
"args": ["-v", "1", "--log_file", "ahi/missionlogs/2025/20250414_20250418/20250415T040019/202504150400_202504152346.nc4", "--update_ssds_provenance"]
//"args": ["-v", "1", "--log_file", "ahi/missionlogs/2025/20250414_20250418/20250415T040019/202504150400_202504152346.nc4", "--update_ssds_provenance"]
// Make per log file .html files to test with lrauv_deployment_plots and --update_ssds_provenance
//"args": ["-v", "1", "--auv_name", "ahi", "--start", "20251022T000000", "--end", "20251024T000000", "--update_ssds_provenance", "--clobber"]
// Make per log file .html files to test with lrauv_deployment_plots and --update_ssds_provenance to test dynamic map bounds
"args": ["-v", "1", "--auv_name", "ahi", "--start", "20260409T000000", "--end", "20260415T000000", "--update_ssds_provenance", "--clobber"]
},
{
"name": "lrauv_deployment_plots",
Expand Down
38 changes: 32 additions & 6 deletions src/data/create_products.py
Original file line number Diff line number Diff line change
Expand Up @@ -1062,22 +1062,48 @@ def _plot_track_map( # noqa: PLR0915
# Store original position
pos = map_ax.get_position()

# Set fixed axis limits for Monterey Bay area (in Web Mercator) FIRST
lon_bounds = [-122.41, -121.77]
lat_bounds = [36.5, 37.0]
# Use fixed Monterey Bay bounds when the track fits within them; otherwise
# compute bounds dynamically from the data with a 10% margin.
fixed_lon_bounds = [-122.41, -121.77]
fixed_lat_bounds = [36.5, 37.0]
valid_lons = lons[~np.isnan(lons)]
valid_lats = lats[~np.isnan(lats)]
if (
valid_lons.size > 0
and valid_lats.size > 0
and valid_lons.min() >= fixed_lon_bounds[0]
and valid_lons.max() <= fixed_lon_bounds[1]
and valid_lats.min() >= fixed_lat_bounds[0]
and valid_lats.max() <= fixed_lat_bounds[1]
):
lon_bounds = fixed_lon_bounds
lat_bounds = fixed_lat_bounds
else:
lon_margin = (valid_lons.max() - valid_lons.min()) * 0.1 or 0.05
lat_margin = (valid_lats.max() - valid_lats.min()) * 0.1 or 0.05
lon_bounds = [valid_lons.min() - lon_margin, valid_lons.max() + lon_margin]
lat_bounds = [valid_lats.min() - lat_margin, valid_lats.max() + lat_margin]
x_bounds, y_bounds = transformer.transform(lon_bounds, lat_bounds)
map_ax.set_xlim(x_bounds)
map_ax.set_ylim(y_bounds)

# Make the plot square by using equal aspect with explicit box adjustment
map_ax.set_aspect("equal", adjustable="box")

# Plot the track with profile_number coloring in Web Mercator coordinates
# Plot the track colored by a cumulative profile number that keeps
# incrementing across concatenated log files (profile_number resets to
# zero at the start of each log file).
profile_numbers = self.ds["profile_number"].to_numpy()
cumulative_profile = profile_numbers.copy().astype(float)
offset = 0
for i in range(1, len(profile_numbers)):
if profile_numbers[i] < profile_numbers[i - 1]:
offset += profile_numbers[i - 1]
cumulative_profile[i] = profile_numbers[i] + offset
scatter = map_ax.scatter(
x_merc,
y_merc,
c=profile_numbers,
c=cumulative_profile,
cmap="jet",
s=1,
alpha=0.6,
Expand All @@ -1104,7 +1130,7 @@ def _plot_track_map( # noqa: PLR0915
# Now position map aligned with left edge of reference, 50% width
# Use a square aspect ratio based on the y-dimension
map_height = pos.height
aspect_ratio = (37.0 - 36.5) / (122.41 - 121.77) # data aspect ratio
aspect_ratio = (lat_bounds[1] - lat_bounds[0]) / (lon_bounds[1] - lon_bounds[0])
map_width = map_height / aspect_ratio * 0.7 # scale to fit nicely

# Align map top with the nighttime indicator top when ref axes is available.
Expand Down
40 changes: 17 additions & 23 deletions src/data/lrauv_deployment_plots.py
Original file line number Diff line number Diff line change
Expand Up @@ -159,36 +159,23 @@ def _concat_datasets(self, nc_files: list[Path | str]) -> xr.Dataset | None:

Accepts both local Paths and OPeNDAP URL strings — xr.open_dataset
handles both transparently.
Tries xr.open_mfdataset first; falls back to manual concat on failure.
Uses join='outer' so every variable present in any log_file is retained
(absent values filled with NaN).
"""
if not nc_files:
return None

paths = [str(p) for p in nc_files]
try:
self.logger.info("Concatenating %d files via open_mfdataset", len(paths))
return xr.open_mfdataset(
paths,
combine="by_coords",
join="outer",
parallel=True,
chunks="auto",
)
except Exception as exc: # noqa: BLE001
self.logger.warning("open_mfdataset failed (%s), falling back to xr.concat", exc)

datasets = []
for p in nc_files:
try:
datasets.append(xr.open_dataset(p))
datasets.append(xr.open_dataset(str(p)))
except OSError as e:
self.logger.warning("Skipping %s: %s", p, e)

if not datasets:
return None

self.logger.info("Concatenating %d dataset(s) via xr.concat", len(datasets))
return xr.concat(datasets, dim="time", join="outer")

def _deployment_has_outputs(self, deployment_dir: Path, plot_name_stem: str) -> bool:
Expand Down Expand Up @@ -408,17 +395,21 @@ def _send_notify_email(
std_png = None
web_url = get_web_url(str(std_html)) if std_html else ""

sent_on = datetime.now(tz=UTC).astimezone().strftime("%Y-%m-%d %H:%M:%S %Z")
plain = (
f"New LRAUV deployment plots: {deployment_name}\n\n"
f" View this and related information on the web:\n {web_url}"
f" View this and related information on the web:\n {web_url}\n\n"
f"Sent on: {sent_on}"
)
img_tag = (
'<img src="cid:std_plot" alt="Standard plot" style="max-width:100%"><br>'
if std_png
else ""
)
html_body = (
f'{img_tag}<p><a href="{web_url}">View this and related information on the web</a></p>'
f"{img_tag}"
f'<p><a href="{web_url}">View this and related information on the web</a></p>'
f"<p><small>Sent on: {sent_on}</small></p>"
)

outer = MIMEMultipart("related")
Expand Down Expand Up @@ -608,6 +599,7 @@ def _write_per_png_html( # noqa: C901, PLR0913
grouped.setdefault(log_dir, []).append(url)

# Collect per-row data first so we can suppress empty columns
nt = 'target="_blank" rel="noopener"' # new-tab attributes for all links
rows: list[dict] = []
for log_dir in sorted(grouped):
nc_urls = grouped[log_dir]
Expand All @@ -616,9 +608,11 @@ def _write_per_png_html( # noqa: C901, PLR0913
rows.append(
{
"dir": log_dir,
"plots": "<br>".join(f'<a href="{u}">{lbl}</a>' for u, lbl in png_links),
"dap": "".join(f'<a href="{nc_url}.html">OPeNDAP</a>' for nc_url in nc_urls),
"stoqs": f'<a href="{log_stoqs_url}">STOQS</a>' if log_stoqs_url else "",
"plots": "<br>".join(f'<a href="{u}" {nt}>{lbl}</a>' for u, lbl in png_links),
"dap": "".join(
f'<a href="{nc_url}.html" {nt}>OPeNDAP</a>' for nc_url in nc_urls
),
"stoqs": f'<a href="{log_stoqs_url}" {nt}>STOQS</a>' if log_stoqs_url else "",
}
)

Expand Down Expand Up @@ -646,7 +640,7 @@ def _write_per_png_html( # noqa: C901, PLR0913
other_plots_line = ""
if other_png_paths:
sibling_links = [
f'<a href="{Path(p).with_suffix(".html").name}">{Path(p).name}</a>'
f'<a href="{Path(p).with_suffix(".html").name}" {nt}>{Path(p).name}</a>'
for p in other_png_paths
if Path(p).exists()
]
Expand All @@ -659,7 +653,7 @@ def _write_per_png_html( # noqa: C901, PLR0913
if stoqs_url:
after_scheme = stoqs_url.split("//", 1)[-1] if "//" in stoqs_url else stoqs_url
db_label = after_scheme.split("/")[1] if "/" in after_scheme else after_scheme
stoqs_line = f'<p>View these data in <a href="{stoqs_url}">{db_label}</a></p>\n'
stoqs_line = f'<p>View these data in <a href="{stoqs_url}" {nt}>{db_label}</a></p>\n'

if png_file_path is not None and png_file_path.exists():
b64 = base64.b64encode(png_file_path.read_bytes()).decode("ascii")
Expand All @@ -673,7 +667,7 @@ def _write_per_png_html( # noqa: C901, PLR0913
footer = (
"<hr>\n"
"<p><small>Created by "
f'<a href="{script_github_url}">lrauv_deployment_plots.py</a>'
f'<a href="{script_github_url}" {nt}>lrauv_deployment_plots.py</a>'
f" on {created_ts}</small></p>\n"
)
html = (
Expand Down
5 changes: 3 additions & 2 deletions src/data/make_permalink.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import csv
import json
import sys
from datetime import UTC, datetime
from datetime import UTC, datetime, timedelta
from pathlib import Path

import lzstring
Expand Down Expand Up @@ -96,7 +96,8 @@ def stoqs_url_from_ds(ds: xr.Dataset, base_url: str | None = None, auv_name: str
# --- times ---
times_np = ds.cf["time"].to_numpy()
stime = pd.Timestamp(times_np[0]).to_pydatetime().replace(tzinfo=UTC)
etime = pd.Timestamp(times_np[-1]).to_pydatetime().replace(tzinfo=UTC)
# Subtract 1 second from the end time to avoid picking up the next Activity in the STOQS UI
etime = pd.Timestamp(times_np[-1]).to_pydatetime().replace(tzinfo=UTC) - timedelta(seconds=1)

# --- depths ---
depths_np = ds.cf["depth"].to_numpy()
Expand Down
Loading