From 55fbf83dc825fe5684bf63ec5b3a00c68d250eee Mon Sep 17 00:00:00 2001 From: Mike McCann Date: Tue, 19 May 2026 15:14:41 -0700 Subject: [PATCH 1/2] Add downwelling_photosynthetic_photon_flux_in_sea_water to sbd's root group. --- .vscode/launch.json | 4 +++- src/data/sbd2netcdf.py | 2 ++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 2369e22..04a47ed 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -502,9 +502,11 @@ // Test a month with fewer plots to make //"args": ["--auv_name", "daphne", "--start", "20260101", "--end", "20260131", "-v", "2", "--clobber"] // Test pyxis - "args": ["--auv_name", "pyxis", "--start", "20260501", "--end", "20260531", "-v", "1", "--clobber"] + //"args": ["--auv_name", "pyxis", "--start", "20260501", "--end", "20260531", "-v", "1", "--clobber"] // Test --current_month //"args": ["--current_month", "-v", "1"] + // Test reading downwelling_photosynthetic_photon_flux_in_sea_water from the root group of the netCDF file instead of the instrument group, which was the case for some early daphne log files + "args": ["--auv_name", "daphne", "--start", "20260501", "--end", "20260531", "-v", "1", "--clobber"] }, { "name": "lrauv_deployment_plots", diff --git a/src/data/sbd2netcdf.py b/src/data/sbd2netcdf.py index ccba3f8..f75e47c 100755 --- a/src/data/sbd2netcdf.py +++ b/src/data/sbd2netcdf.py @@ -46,6 +46,7 @@ "time_fix", "latitude_fix", "longitude_fix", + "downwelling_photosynthetic_photon_flux_in_sea_water", ], "CTD_Seabird": [ "bin_median_sea_water_temperature", @@ -81,6 +82,7 @@ "time_fix": "nal9602", "latitude_fix": "nal9602", "longitude_fix": "nal9602", + "downwelling_photosynthetic_photon_flux_in_sea_water": "parlicor", } # Coordinates from the root group — no prefix applied From 9465ceba4724e3b9fb1608ed62e4a0ebdbfda83c Mon Sep 17 00:00:00 2001 From: Mike McCann Date: Tue, 19 May 2026 16:02:10 -0700 Subject: [PATCH 2/2] Make a separate _cbit.png page for the cbit_amphoursused plot. --- .vscode/launch.json | 6 +- src/data/create_products.py | 199 +++++++++++++++++++++++++--------- src/data/process_lrauv_sbd.py | 2 + 3 files changed, 154 insertions(+), 53 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 04a47ed..1027292 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -501,12 +501,12 @@ //"args": ["--auv_name", "ahi", "--start", "20260303", "--end", "20260331", "-v", "2", "--clobber"] // Test a month with fewer plots to make //"args": ["--auv_name", "daphne", "--start", "20260101", "--end", "20260131", "-v", "2", "--clobber"] - // Test pyxis - //"args": ["--auv_name", "pyxis", "--start", "20260501", "--end", "20260531", "-v", "1", "--clobber"] + // Test pyxis and cbit_amphoursused plot + "args": ["--auv_name", "pyxis", "--start", "20260501", "--end", "20260531", "-v", "1", "--clobber"] // Test --current_month //"args": ["--current_month", "-v", "1"] // Test reading downwelling_photosynthetic_photon_flux_in_sea_water from the root group of the netCDF file instead of the instrument group, which was the case for some early daphne log files - "args": ["--auv_name", "daphne", "--start", "20260501", "--end", "20260531", "-v", "1", "--clobber"] + //"args": ["--auv_name", "daphne", "--start", "20260501", "--end", "20260531", "-v", "1", "--clobber"] }, { "name": "lrauv_deployment_plots", diff --git a/src/data/create_products.py b/src/data/create_products.py index 877b7c6..d5be003 100755 --- a/src/data/create_products.py +++ b/src/data/create_products.py @@ -613,7 +613,6 @@ def _get_planktivore_plot_variables(self) -> list: def _get_lrauv_engineering_plot_variables(self) -> list: """Get LRAUV engineering/platform variables for plot_engineering_2column().""" - is_pyxis_realtime = self.auv_name == "pyxis" and self.mission and "realtime" in self.mission return [ ("bpc1_platform_battery_charge", "linear"), ("bpc1_platform_battery_voltage", "linear"), @@ -622,12 +621,7 @@ def _get_lrauv_engineering_plot_variables(self) -> list: ("universals_platform_yaw_angle", "linear"), ("universals_platform_roll_angle", "linear"), ("buoyancyservo_platform_buoyancy_position", "linear"), - ( - "cbit_amphoursused" - if is_pyxis_realtime - else "elevatorservo_platform_elevator_angle", - "linear", - ), + ("elevatorservo_platform_elevator_angle", "linear"), ("massservo_platform_mass_position", "linear"), ] @@ -853,7 +847,12 @@ def sbd_png_paths(self) -> list[Path]: """ if not self.output_dir or not self.plot_name_stem: return [] - suffixes = ("_2column_cmocean", "_2column_planktivore", "_2column_engineering") + suffixes = ( + "_2column_cmocean", + "_2column_planktivore", + "_2column_engineering", + "_2column_cbit", + ) return [ p for suffix in suffixes @@ -1447,11 +1446,41 @@ def _overlay_sipper_on_map(self, map_ax: matplotlib.axes.Axes, transformer) -> N ) first = False - def _plot_track_map( # noqa: PLR0915 + def _section_overlays(self, distnav: xr.DataArray) -> tuple: + """Return (profile_bottoms, bottom_depths) for section plots, None on error.""" + try: + profile_bottoms = self._profile_bottoms(distnav) + except (TypeError, ValueError) as e: + self.logger.warning("Error computing profile bottoms: %s", e) # noqa: TRY400 + profile_bottoms = None + try: + bottom_depths = self._get_bathymetry( + self.ds.cf["longitude"].to_numpy(), + self.ds.cf["latitude"].to_numpy(), + ) + except ValueError as e: # noqa: BLE001 + self.logger.warning("Error retrieving bathymetry: %s", e) # noqa: TRY400 + bottom_depths = None + return profile_bottoms, bottom_depths + + def _cumulative_profile_numbers(self) -> np.ndarray: + """Return profile_number array that increments monotonically across log files.""" + profile_numbers = self.ds["profile_number"].to_numpy() + result = 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] + result[i] = profile_numbers[i] + offset + return result + + def _plot_track_map( # noqa: PLR0915, PLR0913 self, map_ax: matplotlib.axes.Axes, reference_ax: matplotlib.axes.Axes, night_ref_ax: matplotlib.axes.Axes | None = None, + color_data: np.ndarray | None = None, + color_label: str | None = None, ) -> None: """Plot AUV track map on left side with title and times on right. @@ -1534,21 +1563,17 @@ 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 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 + # Color the track by caller-supplied data when provided, otherwise by + # cumulative profile number (keeps incrementing across concatenated files). + if color_data is not None: + c_values, cmap_name = color_data, "hot_r" + else: + c_values, cmap_name = self._cumulative_profile_numbers(), "jet" scatter = map_ax.scatter( x_merc, y_merc, - c=cumulative_profile, - cmap="jet", + c=c_values, + cmap=cmap_name, s=1, alpha=0.6, ) @@ -1607,7 +1632,7 @@ def _plot_track_map( # noqa: PLR0915 cax=cbar_ax, orientation="vertical", ) - cbar.set_label("Profile Number", fontsize=9) + cbar.set_label(color_label if color_label is not None else "Profile Number", fontsize=9) cbar.ax.tick_params(labelsize=8) # Remove axes, labels, and ticks but keep the border @@ -2981,20 +3006,7 @@ def plot_planktivore_2column(self) -> str: # noqa: C901, PLR0912, PLR0915 self.logger.warning("Error retrieving sipper locations: %s", e) # noqa: TRY400 gulper_locations = {} - try: - profile_bottoms = self._profile_bottoms(distnav) - except (TypeError, ValueError) as e: - self.logger.warning("Error computing profile bottoms: %s", e) # noqa: TRY400 - profile_bottoms = None - - try: - bottom_depths = self._get_bathymetry( - self.ds.cf["longitude"].to_numpy(), - self.ds.cf["latitude"].to_numpy(), - ) - except ValueError as e: # noqa: BLE001 - self.logger.warning("Error retrieving bathymetry: %s", e) # noqa: TRY400 - bottom_depths = None + profile_bottoms, bottom_depths = self._section_overlays(distnav) row = 1 col = 0 @@ -3105,20 +3117,7 @@ def plot_engineering_2column(self) -> str: # noqa: C901, PLR0912, PLR0915 self._plot_track_map(ax[0, 0], ax[1, 0], ax[0, 1]) - try: - profile_bottoms = self._profile_bottoms(distnav) - except (TypeError, ValueError) as e: - self.logger.warning("Error computing profile bottoms: %s", e) # noqa: TRY400 - profile_bottoms = None - - try: - bottom_depths = self._get_bathymetry( - self.ds.cf["longitude"].to_numpy(), - self.ds.cf["latitude"].to_numpy(), - ) - except ValueError as e: # noqa: BLE001 - self.logger.warning("Error retrieving bathymetry: %s", e) # noqa: TRY400 - bottom_depths = None + profile_bottoms, bottom_depths = self._section_overlays(distnav) row = 1 col = 0 @@ -3182,6 +3181,106 @@ def plot_engineering_2column(self) -> str: # noqa: C901, PLR0912, PLR0915 self.logger.info("Saved engineering 2column plot to %s", output_file) return str(output_file) + def plot_cbit_2column(self) -> str: + """Create a 3-panel plot for cbit_amphoursused: track map, distance scatter, time series.""" + _CBIT_VAR = "cbit_amphoursused" + + if "pytest" in sys.modules: + return None + if not self._is_lrauv(): + return None + + self._open_ds() + + if _CBIT_VAR not in self.ds: + self.logger.debug("%s not in dataset, skipping plot_cbit_2column", _CBIT_VAR) + return None + + cbit = self.ds[_CBIT_VAR].dropna("time") + if cbit.size == 0: + self.logger.debug("No valid %s data, skipping plot_cbit_2column", _CBIT_VAR) + return None + + idist, iz, distnav = self._grid_dims([_CBIT_VAR]) + if distnav.size == 0: + self.logger.warning("Skipping plot_cbit_2column due to missing distnav") + return None + + units = self.ds[_CBIT_VAR].attrs.get("units", "Ah") + ylabel = f"Amp-hours Used ({units})" + + cbit_at_nav = cbit.interp(time=distnav.coords["time"], method="linear").to_numpy() + times = pd.to_datetime(cbit.coords["time"].to_numpy()) + cbit_vals = cbit.to_numpy() + + fig = plt.figure(figsize=(18, 8)) + gs = matplotlib.gridspec.GridSpec(2, 2, figure=fig, hspace=0.35, wspace=0.15) + ax_map = fig.add_subplot(gs[0, 0]) + ax_section = fig.add_subplot(gs[0, 1]) + ax_time = fig.add_subplot(gs[1, :]) + + # Track map colored by Ah Used + self._plot_track_map( + ax_map, + ax_time, + ax_section, + color_data=cbit_at_nav, + color_label=ylabel, + ) + + # Depth-vs-distance section colored by Ah Used (same style as other plot pages) + profile_bottoms, bottom_depths = self._section_overlays(distnav) + + ax_array = np.array([[ax_map, ax_section]]) + self._plot_var( + _CBIT_VAR, + idist, + iz, + distnav, + fig, + ax_array, + 0, + 1, + profile_bottoms, + scale="linear", + gulper_locations={}, + bottom_depths=bottom_depths, + best_ctd=None, + ) + ax_section.set_xlabel("Distance along track (km)") + + # Time series + ax_time.plot(times, cbit_vals, color="tab:red", linewidth=1) + ax_time.set_xlabel("Time (UTC)") + ax_time.set_ylabel(ylabel) + ax_time.set_title("Ah Used vs. Time") + ax_time.xaxis.set_major_formatter(matplotlib.dates.DateFormatter("%Y-%m-%d\n%H:%M")) + plt.setp(ax_time.get_xticklabels(), rotation=0, ha="center", fontsize=8) + + out_dir = ( + self.output_dir + if self.output_dir is not None + else Path(BASE_LRAUV_PATH, f"{Path(self.log_file).parent}") + ) + out_dir.mkdir(parents=True, exist_ok=True) + stem = self.plot_name_stem if self.plot_name_stem is not None else Path(self.log_file).stem + output_file = Path(out_dir, f"{stem}_{self.freq}_2column_cbit.png") + created = datetime.now().astimezone().strftime("%Y-%m-%d %H:%M:%S %Z") + fig.text( + 0, + 0, + f"Created by auv-python's create_products.py on {created}.", + fontsize=6, + fontstyle="italic", + ha="left", + va="bottom", + ) + plt.savefig(output_file, dpi=100, bbox_inches="tight") + plt.close(fig) + + self.logger.info("Saved cbit 2column plot to %s", output_file) + return str(output_file) + def _get_best_ctd(self) -> str: """Determine best CTD to use for ODV lookup table based on metadata""" # LRAUV doesn't use multiple CTDs, return None diff --git a/src/data/process_lrauv_sbd.py b/src/data/process_lrauv_sbd.py index 23a9e73..152fe78 100755 --- a/src/data/process_lrauv_sbd.py +++ b/src/data/process_lrauv_sbd.py @@ -215,6 +215,7 @@ def _make_per_log_plots( cp.plot_2column() cp.plot_planktivore_2column() cp.plot_engineering_2column() + cp.plot_cbit_2column() except Exception as e: # noqa: BLE001 logger.warning("Per-log plot failed for %s: %s", p.name, e) @@ -266,6 +267,7 @@ def _make_products( cp.plot_2column() cp.plot_planktivore_2column() cp.plot_engineering_2column() + cp.plot_cbit_2column() png_paths = cp.sbd_png_paths() if png_paths: