diff --git a/.vscode/launch.json b/.vscode/launch.json index 71250ea..c2319c2 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -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", diff --git a/src/data/create_products.py b/src/data/create_products.py index 55b5345..6295bcf 100755 --- a/src/data/create_products.py +++ b/src/data/create_products.py @@ -1062,9 +1062,27 @@ 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) @@ -1072,12 +1090,20 @@ def _plot_track_map( # noqa: PLR0915 # 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, @@ -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. diff --git a/src/data/lrauv_deployment_plots.py b/src/data/lrauv_deployment_plots.py index 9370238..19321dd 100755 --- a/src/data/lrauv_deployment_plots.py +++ b/src/data/lrauv_deployment_plots.py @@ -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: @@ -408,9 +395,11 @@ 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 = ( 'Standard plot
' @@ -418,7 +407,9 @@ def _send_notify_email( else "" ) html_body = ( - f'{img_tag}

View this and related information on the web

' + f"{img_tag}" + f'

View this and related information on the web

' + f"

Sent on: {sent_on}

" ) outer = MIMEMultipart("related") @@ -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] @@ -616,9 +608,11 @@ def _write_per_png_html( # noqa: C901, PLR0913 rows.append( { "dir": log_dir, - "plots": "
".join(f'{lbl}' for u, lbl in png_links), - "dap": "".join(f'OPeNDAP' for nc_url in nc_urls), - "stoqs": f'STOQS' if log_stoqs_url else "", + "plots": "
".join(f'{lbl}' for u, lbl in png_links), + "dap": "".join( + f'OPeNDAP' for nc_url in nc_urls + ), + "stoqs": f'STOQS' if log_stoqs_url else "", } ) @@ -646,7 +640,7 @@ def _write_per_png_html( # noqa: C901, PLR0913 other_plots_line = "" if other_png_paths: sibling_links = [ - f'{Path(p).name}' + f'{Path(p).name}' for p in other_png_paths if Path(p).exists() ] @@ -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'

View these data in {db_label}

\n' + stoqs_line = f'

View these data in {db_label}

\n' if png_file_path is not None and png_file_path.exists(): b64 = base64.b64encode(png_file_path.read_bytes()).decode("ascii") @@ -673,7 +667,7 @@ def _write_per_png_html( # noqa: C901, PLR0913 footer = ( "
\n" "

Created by " - f'lrauv_deployment_plots.py' + f'lrauv_deployment_plots.py' f" on {created_ts}

\n" ) html = ( diff --git a/src/data/make_permalink.py b/src/data/make_permalink.py index 2052ee0..24129d4 100755 --- a/src/data/make_permalink.py +++ b/src/data/make_permalink.py @@ -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 @@ -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()