diff --git a/.gitignore b/.gitignore index 5a70c22..647b0aa 100644 --- a/.gitignore +++ b/.gitignore @@ -6,6 +6,9 @@ data/temp.nc logs/*log +# Claude Code configuration +.claude/ + # VSCode settings .vscode/* diff --git a/docs/source/clock_offset.rst b/docs/source/clock_offset.rst new file mode 100644 index 0000000..7182dcb --- /dev/null +++ b/docs/source/clock_offset.rst @@ -0,0 +1,100 @@ +Clock Offset Analysis +===================== + +Clock offset analysis is a critical intermediate step in oceanographic data processing that occurs between Stage 1 (standardization) and Stage 2 (trimming to deployment) of the processing pipeline. This analysis identifies and corrects timing errors in instrument clocks to ensure accurate temporal alignment of data from multiple instruments on the same mooring. + +Overview +-------- + +Oceanographic instruments deployed on moorings may have clock offsets due to incorrect setup, clock drift, or other timing issues. Since scientific analysis often requires precise temporal correlation between instruments at different depths, these timing errors must be identified and corrected. + +The clock offset analysis provides two complementary methods to detect timing discrepancies: + +1. **Deployment Period Detection**: Uses temperature profiles to identify when instruments were actually deployed on the seafloor +2. **Lag Correlation Analysis**: Compares temperature time series between instruments to detect systematic timing offsets + +Methods +------- + +Deployment Period Detection +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +This method leverages the characteristic temperature signature that occurs when instruments transition from surface conditions to deep-water conditions during deployment: + +- **Temperature Threshold**: Identifies "cold" water temperatures using statistical analysis (typically mean ± 3 standard deviations of deep values) +- **Start/End Times**: Determines when each instrument first and last recorded temperatures within the cold water range +- **Consensus Analysis**: Groups instruments with similar deployment timing to establish reference times +- **Offset Calculation**: Computes timing differences relative to the consensus group + +This approach is effective for detecting large clock offsets (hours to days) and works well when instruments show clear temperature transitions during deployment and recovery. + +Lag Correlation Analysis +~~~~~~~~~~~~~~~~~~~~~~~~ + +This method performs cross-correlation analysis between temperature time series from different instruments: + +- **Reference Selection**: Uses one instrument as a temporal reference (typically the deepest or most reliable) +- **Subsampling**: Reduces computational load by subsampling time series while maintaining correlation structure +- **Cross-Correlation**: Computes lag correlations to find the temporal offset that maximizes correlation between instruments +- **Peak Detection**: Identifies the lag corresponding to maximum correlation as the estimated clock offset + +This approach is sensitive to smaller timing errors (seconds to minutes) and works well when instruments record similar environmental variations. + +Implementation +-------------- + +The analysis is implemented in the ``oceanarray.clock_offset`` module with the following key functions: + +Core Functions +~~~~~~~~~~~~~~ + +- ``load_mooring_instruments()``: Load instrument data and enrich with YAML metadata +- ``analyze_deployment_timing()``: Perform temperature-based deployment period detection +- ``calculate_timing_offsets()``: Calculate offsets using consensus grouping approach +- ``perform_lag_correlation_analysis()``: Execute cross-correlation analysis +- ``print_timing_offset_summary()``: Generate human-readable offset summary + +Workflow Functions +~~~~~~~~~~~~~~~~~~ + +- ``create_common_time_grid()``: Generate interpolation grid for temporal alignment +- ``interpolate_datasets_to_grid()``: Interpolate instrument data to common time base +- ``combine_interpolated_datasets()``: Merge data into multi-level dataset structure + +Usage +----- + +The analysis is demonstrated in the ``demo_clock_offset.ipynb`` notebook, which provides a streamlined workflow: + +1. **Configuration**: Specify mooring name and data paths +2. **Data Loading**: Load instrument datasets and YAML metadata +3. **Preprocessing**: Create common time grid and interpolate data +4. **Deployment Analysis**: Identify deployment periods using temperature profiles +5. **Visualization**: Plot temperature time series with deployment bounds +6. **Offset Calculation**: Compute timing offsets using both methods +7. **Results Summary**: Generate recommendations for YAML configuration + +Output and Application +---------------------- + +The analysis produces: + +- **Offset Estimates**: Recommended clock_offset values (in seconds) for each instrument +- **Quality Metrics**: Correlation coefficients and confidence measures +- **Visualizations**: Time series plots showing deployment periods and timing relationships +- **Summary Tables**: Tabular results comparing both analysis methods + +**Important**: The calculated offsets should be entered as **negative values** in the YAML configuration file, as they represent the correction needed to align instrument times with UTC. + +After updating the YAML file with clock_offset values, Stage 2 processing applies these corrections to create ``*_use.nc`` files. The clock offset analysis can then be re-run using the corrected data to verify that timing discrepancies have been resolved. + +Best Practices +-------------- + +- **Method Comparison**: Always compare results from both deployment detection and lag correlation methods +- **Visual Inspection**: Examine temperature time series plots to validate automated detection +- **Iterative Refinement**: Re-run analysis after applying corrections to verify success +- **Documentation**: Record analysis decisions and unusual findings in processing logs +- **Validation**: Cross-check results with deployment/recovery logs when available + +The clock offset analysis ensures that subsequent processing stages work with temporally aligned data, which is essential for accurate calculation of transport estimates and other derived quantities that depend on precise temporal relationships between measurements at different depths. \ No newline at end of file diff --git a/docs/source/index.rst b/docs/source/index.rst index e5375e1..6c14ee0 100644 --- a/docs/source/index.rst +++ b/docs/source/index.rst @@ -24,6 +24,7 @@ Contents Data Acquisition Standardisation (Stage 1) + Clock Offset Analysis Trim to Deployment (Stage 2) Automatic QC (Stage 3) Apply Calibration (Stage 4) @@ -57,6 +58,7 @@ Contents :caption: Help and reference GitHub Repo + roadmap oceanarray faq diff --git a/docs/source/roadmap.rst b/docs/source/roadmap.rst new file mode 100644 index 0000000..e57ff73 --- /dev/null +++ b/docs/source/roadmap.rst @@ -0,0 +1,345 @@ +================ +Development Roadmap +================ + +This document outlines the development roadmap for the OceanArray processing framework, focusing on features documented in the processing workflow that need implementation, technical improvements, and future functionality priorities. + +.. contents:: + :local: + :depth: 3 + +Status Overview +=============== + +The OceanArray framework currently provides a solid foundation for oceanographic data processing, but several key components documented in the processing framework require implementation or completion. + +**Current Implementation Status:** + +✅ **Implemented & Working** + - Stage 1: Standardisation (``stage1.py``) + - Stage 2: Trimming & Clock Corrections (``stage2.py``) + - Step 1: Time Gridding (``time_gridding.py``) + - Clock Offset Analysis (``clock_offset.py``) + - Data Readers (``readers.py``) + - Basic QC visualization (``plotters.py``) + - Configurable Logging System + +🟡 **Partially Implemented** + - Stage 3: Auto QC - basic QARTOD functions exist (``tools.py``) + - Stage 4: Calibration - microcat calibration exists (``instrument.py``) + - Step 2: Vertical Gridding - physics-based interpolation exists (``rapid_interp.py``) + +❌ **Documented but Not Implemented** + - Stage 4: Conversion to OceanSites/AC1 format + - Step 3: Concatenation of deployments + - Multi-site merging for boundary profiles + - Comprehensive automatic QC framework + - Transport calculations and MOC diagnostics + +Priority 1: Core Missing Features +================================= + +1. Stage 3: Comprehensive Auto QC Framework +------------------------------------------- + +**Documentation**: ``docs/source/methods/auto_qc.rst`` + +**Current State**: Basic QARTOD functions exist in ``tools.py:run_qc()`` and visualization in ``plotters.py:plot_qartod_summary()``. + +**Missing Implementation**: +- Structured QC configuration system +- Integration with ``ioos_qc`` package as documented +- Complete flag value handling (0,1,2,3,4,7,8,9) +- Automated QC report generation +- QC metadata preservation in datasets + +**Estimated Effort**: 2-3 weeks + +**Implementation Plan**: + 1. Create ``oceanarray/auto_qc.py`` module + 2. Design YAML-based QC configuration system + 3. Implement comprehensive flag handling + 4. Add QC validation and reporting + 5. Integrate with existing Stage 2 workflow + +2. Stage 4: OceanSites/AC1 Format Conversion +-------------------------------------------- + +**Documentation**: ``docs/source/methods/conversion.rst`` + +**Current State**: Some format conversion exists in ``convertOS.py``, but not the full AC1 specification. + +**Missing Implementation**: +- Complete AC1 format specification compliance +- Global attribute validation and enforcement +- CF-convention compliance checking +- Variable attribute standardization +- Comprehensive metadata handling + +**Estimated Effort**: 2-3 weeks + +**Implementation Plan**: + 1. Create ``oceanarray/conversion.py`` module + 2. Implement AC1 format validation + 3. Add CF-compliance checking + 4. Design metadata template system + 5. Add format conversion pipeline + +3. Step 3: Deployment Concatenation +----------------------------------- + +**Documentation**: ``docs/source/methods/concatenation.rst`` + +**Current State**: No implementation found. + +**Missing Implementation**: +- Multi-deployment time series merging +- Gap handling and interpolation +- Consistent time-pressure grid creation +- Metadata preservation across deployments +- Quality flag propagation + +**Estimated Effort**: 1-2 weeks + +**Implementation Plan**: + 1. Create ``oceanarray/concatenation.py`` module + 2. Design deployment merging algorithm + 3. Implement gap filling strategies + 4. Add time-pressure grid standardization + 5. Create validation and QC checks + +Priority 2: Advanced Processing Features +======================================= + +4. Multi-site Merging for Boundary Profiles +------------------------------------------- + +**Documentation**: ``docs/source/methods/multisite_merging.rst`` + +**Current State**: No implementation found. + +**Missing Implementation**: +- Cross-site data integration +- Boundary profile construction +- Static stability checking +- Site-specific weighting strategies +- Spatial interpolation methods + +**Estimated Effort**: 3-4 weeks + +**Implementation Plan**: + 1. Create ``oceanarray/multisite_merging.py`` module + 2. Implement spatial merging algorithms + 3. Add static stability validation + 4. Design site weighting strategies + 5. Create boundary profile outputs + +5. Complete Vertical Gridding Integration +----------------------------------------- + +**Documentation**: ``docs/source/methods/vertical_gridding.rst`` + +**Current State**: Physics-based interpolation exists in ``rapid_interp.py`` but needs integration. + +**Missing Implementation**: +- Integration with main processing pipeline +- Climatology data management +- Configuration for different interpolation strategies +- Gap filling and extrapolation options +- Validation against known profiles + +**Estimated Effort**: 1-2 weeks + +**Implementation Plan**: + 1. Refactor ``rapid_interp.py`` for general use + 2. Create configuration system for interpolation parameters + 3. Add climatology data handling + 4. Integrate with mooring processing workflow + 5. Add validation and diagnostic tools + +Priority 3: Enhanced Calibration System +====================================== + +6. Comprehensive Calibration Framework +-------------------------------------- + +**Documentation**: ``docs/source/methods/calibration.rst`` + +**Current State**: Basic microcat calibration exists in ``instrument.py``. + +**Missing Implementation**: +- Multi-instrument calibration support (not just microcat) +- Structured calibration metadata handling +- Pre/post-cruise comparison workflows +- Calibration uncertainty propagation +- Automated calibration log parsing + +**Estimated Effort**: 2-3 weeks + +**Implementation Plan**: + 1. Expand ``instrument.py`` calibration functions + 2. Create calibration configuration system + 3. Add uncertainty propagation + 4. Design calibration workflow automation + 5. Add comprehensive logging and provenance + +Priority 4: System Architecture Improvements +============================================ + +7. Methods Module Organization +------------------------------ + +**Current State**: Processing functions scattered across multiple modules. + +**Improvement**: Create organized ``methods/`` directory structure: + +.. code-block:: text + + oceanarray/methods/ + ├── __init__.py + ├── auto_qc.py + ├── calibration.py + ├── concatenation.py + ├── conversion.py + ├── multisite_merging.py + └── vertical_gridding.py + +**Estimated Effort**: 1 week + +8. Enhanced Configuration System +-------------------------------- + +**Current State**: Basic logging configuration exists. + +**Missing Features**: +- Global processing configuration +- Site-specific parameter management +- Processing pipeline configuration +- Validation and schema checking + +**Estimated Effort**: 1-2 weeks + +9. Comprehensive Testing Suite +------------------------------ + +**Current State**: Basic tests exist in ``tests/`` directory. + +**Missing Features**: +- End-to-end pipeline testing +- Method-specific unit tests +- Configuration validation tests +- Performance benchmarking + +**Estimated Effort**: 2-3 weeks (ongoing) + +Priority 5: Advanced Analysis Features +===================================== + +10. Transport Calculation Framework +----------------------------------- + +**Documentation**: Transport calculations mentioned in ``processing_framework.rst`` + +**Current State**: Some transport code exists in ``transports.py``. + +**Missing Implementation**: +- TEOS-10 conversion utilities +- Dynamic height calculations +- Geostrophic shear computation +- Mass compensation algorithms +- MOC time series generation + +**Estimated Effort**: 4-6 weeks + +**Implementation Plan**: + 1. Expand ``transports.py`` functionality + 2. Add TEOS-10 integration + 3. Implement dynamic height calculations + 4. Create MOC diagnostic tools + 5. Add transport validation methods + +Development Milestones +===================== + +Phase 1: Core Framework Completion (Months 1-3) +----------------------------------------------- +- Complete auto QC framework +- Implement AC1 format conversion +- Add deployment concatenation +- Organize methods module structure +- Enhance configuration system + +Phase 2: Advanced Processing (Months 4-6) +----------------------------------------- +- Implement multi-site merging +- Complete vertical gridding integration +- Enhance calibration framework +- Expand testing suite + +Phase 3: Analysis Tools (Months 7-9) +------------------------------------ +- Implement transport calculations +- Add MOC diagnostic tools +- Create comprehensive documentation +- Performance optimization + +Technical Debt and Maintenance +============================= + +Ongoing Improvements +------------------- + +1. **Code Quality** + - Add type hints throughout codebase + - Improve error handling and validation + - Standardize documentation strings + - Enhance logging throughout pipeline + +2. **Performance** + - Profile processing bottlenecks + - Optimize memory usage for large datasets + - Add parallel processing capabilities + - Implement caching strategies + +3. **User Experience** + - Create command-line interface + - Add progress indicators for long operations + - Improve error messages and debugging + - Create tutorial notebooks + +4. **Documentation** + - Complete API documentation + - Add processing examples + - Create troubleshooting guides + - Document best practices + +Dependencies and External Integration +==================================== + +Key External Dependencies +------------------------ +- ``ioos_qc``: For comprehensive QC implementation +- ``gsw`` (TEOS-10): For seawater property calculations +- ``verticalnn``: For physics-based vertical interpolation +- ``xarray`` & ``netCDF4``: Core data handling +- ``dask``: For large dataset processing (future) + +Integration Opportunities +------------------------ +- **ERDDAP**: Direct data ingestion capabilities +- **Pangaea**: Data publication workflows +- **OceanSites**: Enhanced format compliance +- **Cloud platforms**: Scalable processing deployment + +Community and Collaboration +=========================== + +Contribution Priorities +----------------------- +1. Documentation of existing RAPID/OSNAP workflows +2. Method validation with known datasets +3. Cross-array compatibility testing +4. Performance benchmarking +5. User interface development + +This roadmap provides a structured path toward completing the OceanArray processing framework while maintaining focus on documented requirements and practical implementation priorities. \ No newline at end of file diff --git a/notebooks/demo_clock_offset.ipynb b/notebooks/demo_clock_offset.ipynb new file mode 100644 index 0000000..861d2f4 --- /dev/null +++ b/notebooks/demo_clock_offset.ipynb @@ -0,0 +1,359 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "streamlined-demo", + "metadata": {}, + "source": [ + "# Demo: Streamlined Clock Offset Analysis\n", + "\n", + "This notebook provides a streamlined version of clock offset analysis for oceanographic instruments.\n", + "It uses the new `oceanarray.clock_offset` module for cleaner, more maintainable code.\n", + "\n", + "## Purpose\n", + "\n", + "This notebook helps determine whether instrument timestamps are incorrect by:\n", + "1. Analyzing deployment timing based on temperature profiles\n", + "2. Performing lag correlation analysis between instruments\n", + "3. Calculating recommended clock offset corrections\n", + "\n", + "**Note:** This notebook does not modify data files. It only analyzes and suggests clock_offset values for the YAML configuration.\n" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "imports", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "import pandas as pd\n", + "from oceanarray import clock_offset" + ] + }, + { + "cell_type": "markdown", + "id": "config", + "metadata": {}, + "source": [ + "## Configuration" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "setup", + "metadata": {}, + "outputs": [], + "source": [ + "# Configuration\n", + "mooring_name = 'dsE_1_2018'\n", + "base_dir = '/Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/'\n", + "output_path = base_dir + 'moor/proc/'\n", + "\n", + "# Choose file type: '_raw' for original data, '_use' for processed data\n", + "file_suffix = '_raw'\n", + "# file_suffix = '_use'\n", + "\n", + "print(f\"Analyzing mooring: {mooring_name}\")\n", + "print(f\"Using files with suffix: {file_suffix}\")" + ] + }, + { + "cell_type": "markdown", + "id": "load-data", + "metadata": {}, + "source": [ + "## Load and Process Data" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "load", + "metadata": {}, + "outputs": [], + "source": [ + "# Load instrument data\n", + "datasets, moor_yaml_data = clock_offset.load_mooring_instruments(\n", + " mooring_name, base_dir, output_path, file_suffix\n", + ")\n", + "\n", + "print(f\"Loaded {len(datasets)} instruments\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "process", + "metadata": {}, + "outputs": [], + "source": [ + "# Create common time grid and interpolate\n", + "time_grid = clock_offset.create_common_time_grid(datasets)\n", + "datasets_interp = clock_offset.interpolate_datasets_to_grid(datasets, time_grid)\n", + "\n", + "# Combine into single multi-level dataset\n", + "combined_ds = clock_offset.combine_interpolated_datasets(datasets_interp)\n", + "\n", + "print(f\"Combined dataset shape: {combined_ds.dims}\")\n", + "print(f\"Time grid length: {len(time_grid)}\")\n", + "print(f\"Time range: {time_grid[0]} to {time_grid[-1]}\")" + ] + }, + { + "cell_type": "markdown", + "id": "deployment-analysis", + "metadata": {}, + "source": [ + "## Deployment Timing Analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "deployment", + "metadata": {}, + "outputs": [], + "source": [ + "# Analyze deployment timing using temperature profiles\n", + "combined_ds = clock_offset.analyze_deployment_timing(combined_ds)\n", + "\n", + "print(\"Deployment timing analysis completed\")\n", + "print(f\"Dataset now includes: {list(combined_ds.data_vars)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "visualization", + "metadata": {}, + "source": [ + "## Visualize Temperature Profiles" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "plot-profiles", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot temperature time series with deployment bounds\n", + "time = combined_ds[\"time\"].values\n", + "temp = combined_ds[\"temperature\"].values\n", + "split_vals = combined_ds[\"split_value\"].values\n", + "instruments = combined_ds[\"instrument\"].values\n", + "start_times = combined_ds[\"start_time\"].values\n", + "end_times = combined_ds[\"end_time\"].values\n", + "\n", + "for i in range(combined_ds.dims[\"N_LEVELS\"]):\n", + " fig, ax = plt.subplots(figsize=(12, 4))\n", + "\n", + " ax.plot(time, temp[:, i], label=f\"{instruments[i]}\", alpha=0.7)\n", + " ax.axhline(split_vals[i], color=\"red\", linestyle=\"--\",\n", + " label=f\"Split={split_vals[i]:.2f}\")\n", + "\n", + " # Plot deployment bounds if available\n", + " if np.isfinite(start_times[i].astype(\"datetime64[ns]\").astype(\"int64\")):\n", + " ax.axvline(start_times[i], color=\"green\", linestyle=\"--\", lw=2,\n", + " label=\"Deployment Start\")\n", + " if np.isfinite(end_times[i].astype(\"datetime64[ns]\").astype(\"int64\")):\n", + " ax.axvline(end_times[i], color=\"blue\", linestyle=\"--\", lw=2,\n", + " label=\"Deployment End\")\n", + "\n", + " ax.set_title(f\"Instrument {i}: {instruments[i]} at {combined_ds['nominal_depth'][i].values:.0f}m\")\n", + " ax.set_xlabel(\"Time\")\n", + " ax.set_ylabel(\"Temperature (°C)\")\n", + " ax.legend()\n", + " ax.grid(True, alpha=0.3)\n", + "\n", + " plt.tight_layout()\n", + " plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "timing-offsets", + "metadata": {}, + "source": [ + "## Calculate Timing Offsets" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "j8sqaaoe1ki", + "metadata": {}, + "outputs": [], + "source": [ + "# Calculate timing offsets based on deployment bounds\n", + "offset_results = clock_offset.calculate_timing_offsets(combined_ds)\n", + "\n", + "# Print summary table\n", + "clock_offset.print_timing_offset_summary(combined_ds, offset_results)" + ] + }, + { + "cell_type": "markdown", + "id": "9tcfvxw0pmr", + "metadata": {}, + "source": [ + "## Detailed Deployment Boundary Visualization\n", + "\n", + "Examine the exact transition points with individual measurements around predicted deployment boundaries." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "xihcsh8sa6q", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot detailed deployment boundaries showing individual measurements\n", + "# This shows 10 samples before/after predicted boundaries with red circles and blue connecting lines\n", + "clock_offset.plot_deployment_boundaries(datasets, combined_ds, n_samples=10)" + ] + }, + { + "cell_type": "markdown", + "id": "correlation-analysis", + "metadata": {}, + "source": [ + "## Lag Correlation Analysis" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "correlation", + "metadata": {}, + "outputs": [], + "source": [ + "# Get suggestion for best reference instrument (but you can override manually)\n", + "ref_suggestion = clock_offset.suggest_reference_instrument(combined_ds, offset_results)\n", + "\n", + "# Use the suggested reference (or manually set ref_index to any value you prefer)\n", + "ref_index = ref_suggestion['suggested_index'] # You can change this manually\n", + "sub_sample = 5 # Subsampling factor for speed\n", + "\n", + "print(f\"Using reference instrument: Index {ref_index}\")\n", + "print(f\"Note: You can manually set ref_index to any instrument index (0-{combined_ds.sizes['N_LEVELS']-1})\")\n", + "print()\n", + "\n", + "correlation_results = clock_offset.perform_lag_correlation_analysis(\n", + " combined_ds, ref_index=ref_index, sub_sample=sub_sample\n", + ")\n", + "\n", + "# Print correlation summary\n", + "clock_offset.print_correlation_summary(combined_ds, correlation_results)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "plot-correlations", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot correlation results\n", + "time_interval = correlation_results['time_interval']\n", + "sub_sample = correlation_results['sub_sample']\n", + "depths = combined_ds['nominal_depth'].values\n", + "\n", + "plt.figure(figsize=(12, 6))\n", + "\n", + "max_lag_sub = len(correlation_results['correlations'][0]) // 2\n", + "lags_sub = np.arange(-max_lag_sub, max_lag_sub + 1)\n", + "\n", + "for i, corrs in enumerate(correlation_results['correlations']):\n", + " dt_sub = sub_sample * time_interval\n", + " plt.plot(lags_sub * dt_sub, corrs,\n", + " label=f'Level {i+1} ({depths[i]:.0f}m)', alpha=0.7)\n", + "\n", + "plt.xlabel('Lag (seconds)')\n", + "plt.ylabel('Correlation')\n", + "plt.title(f'Lag Correlation Analysis (Reference: Level {ref_index+1})')\n", + "plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')\n", + "plt.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "summary-plot", + "metadata": {}, + "source": [ + "## Summary: All Temperature Time Series" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "summary", + "metadata": {}, + "outputs": [], + "source": [ + "# Plot all temperature time series together\n", + "plt.figure(figsize=(14, 8))\n", + "\n", + "for i in range(combined_ds.sizes['N_LEVELS']):\n", + " depth = combined_ds['nominal_depth'][i].values\n", + " instrument = combined_ds['instrument'][i].values\n", + " serial = combined_ds['serial_number'][i].values\n", + "\n", + " plt.plot(combined_ds['time'], combined_ds['temperature'][:, i],\n", + " label=f'{instrument} #{serial} ({depth:.0f}m)', alpha=0.8)\n", + "\n", + "plt.xlabel('Time')\n", + "plt.ylabel('Temperature (°C)')\n", + "plt.title(f'Temperature Time Series - {mooring_name}')\n", + "plt.legend(bbox_to_anchor=(1.05, 1), loc='upper left')\n", + "plt.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "recommendations", + "metadata": {}, + "source": [ + "## Recommendations\n", + "\n", + "Based on the analysis above:\n", + "\n", + "1. **Deployment Timing Analysis**: Shows offset estimates based on when instruments first/last detect \"deep\" water temperatures\n", + "2. **Lag Correlation Analysis**: Shows offset estimates based on cross-correlation of temperature time series\n", + "\n", + "Use the **negative** of the calculated offset as the `clock_offset` value in the YAML file.\n", + "\n", + "After updating the YAML file, run stage2 processing to apply the corrections, then re-run this analysis with `file_suffix = '_use'` to verify the corrections." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv (3.11.7)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/notebooks/demo_stage1.ipynb b/notebooks/demo_stage1.ipynb index 21d3467..ee2288c 100644 --- a/notebooks/demo_stage1.ipynb +++ b/notebooks/demo_stage1.ipynb @@ -14,7 +14,7 @@ }, { "cell_type": "code", - "execution_count": 9, + "execution_count": null, "id": "0ff1f4d0", "metadata": {}, "outputs": [], @@ -46,19 +46,10 @@ }, { "cell_type": "code", - "execution_count": 10, + "execution_count": null, "id": "37139297", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Base directory: /Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/\n", - "Processing 1 moorings: ['dsE_1_2018']\n" - ] - } - ], + "outputs": [], "source": [ "# Base directory containing the mooring data\n", "basedir = '/Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/'\n", @@ -73,7 +64,7 @@ "\n", "# Subset for testing\n", "test_moorings_2018 = ['dsA_1_2018', 'dsB_1_2018', 'dsC_1_2018', 'dsD_1_2018', 'dsE_1_2018', 'dsF_1_2018']\n", - "single_test = ['dsE_1_2018']\n", + "single_test = ['dsB_1_2018']\n", "\n", "# Choose which set to process\n", "moorlist = single_test\n", @@ -94,54 +85,10 @@ }, { "cell_type": "code", - "execution_count": 11, + "execution_count": null, "id": "24e4a3c8", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "============================================================\n", - "Processing mooring: dsE_1_2018\n", - "============================================================\n", - "Processing mooring: dsE_1_2018\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6363_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe16/dsE_1_2018_2419_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6401_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6402_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_8482_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6365_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6409_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6397_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6366_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6394_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6370_raw.nc\n", - "EXCEPT: Error reading file moor/raw/msm76_2018/sbe16/DSE18_sbe16_2418.hex: Unknown file type: sbe-hex\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/tr1050/dsE_1_2018_13889_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/rbrsolo/dsE_1_2018_101651_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/tr1050/dsE_1_2018_15580_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/rbrsolo/dsE_1_2018_101647_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/tr1050/dsE_1_2018_13874_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/rbrsolo/dsE_1_2018_101645_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/tr1050/dsE_1_2018_15574_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/rbrsolo/dsE_1_2018_101646_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/tr1050/dsE_1_2018_15577_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/microcat/dsE_1_2018_7518_raw.nc\n", - "OUTFILE EXISTS: Skipping moor/proc/dsE_1_2018/sbe56/dsE_1_2018_6364_raw.nc\n", - "Completed processing: 22/23 instruments successful\n", - "\n", - "Result for dsE_1_2018: ✅ SUCCESS\n", - "\n", - "============================================================\n", - "FINAL PROCESSING SUMMARY\n", - "============================================================\n", - "Successfully processed: 1/1 moorings\n", - "✅ dsE_1_2018\n" - ] - } - ], + "outputs": [], "source": [ "# Initialize the processor\n", "processor = MooringProcessor(basedir)\n", @@ -182,7 +129,7 @@ }, { "cell_type": "code", - "execution_count": 12, + "execution_count": null, "id": "batch_cell", "metadata": {}, "outputs": [], @@ -211,7 +158,7 @@ }, { "cell_type": "code", - "execution_count": 13, + "execution_count": null, "id": "compat_cell", "metadata": {}, "outputs": [], @@ -239,481 +186,10 @@ }, { "cell_type": "code", - "execution_count": 14, + "execution_count": null, "id": "e81775c9", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "Data file: /Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/moor/raw/msm76_2018/aquadopp/DSC18_477102.dat\n", - "Header file: /Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/moor/raw/msm76_2018/aquadopp/DSC18_477102.hdr\n", - "Files exist: data=True, header=True\n", - "\n", - "Dataset loaded successfully!\n", - "Variables: ['Month', 'Day', 'Year', 'Hour', 'Minute', 'Second', 'Error code', 'Status code', 'east_velocity', 'north_velocity', 'up_velocity', 'east_amplitude', 'north_amplitude', 'up_amplitude', 'Battery voltage', 'speed_of_sound', 'Soundspeed used', 'Heading', 'Pitch', 'Roll', 'pressure', 'Pressure_1', 'temperature', 'Analog input 1', 'Analog input 2', 'Speed', 'Direction']\n", - "Time range: 2018-08-12T12:00:00.000000000 to 2018-08-26T17:02:00.000000000\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA1kAAAGwCAYAAABb1a9QAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAknVJREFUeJzt3Qm8VOMbB/CntNO+KlEhSaVsqYiI7EsLlWQrRKIQkZLQgopKIUuLyBYVSiqE/kWKpCRFpV20783/83vHuZ0595yZc2bONjO/7+dz6965c2femXPmnPO87/M+b55IJBIRIiIiIiIickVedx6GiIiIiIiIgEEWERERERGRixhkERERERERuYhBFhERERERkYsYZBEREREREbmIQRYREREREZGLGGQRERERERG5iEEWERERERGRixhkERERERERuYhBFhGlhTfeeEPy5Mkjf/zxR0Y97+OPP64en8K7DySyevVqKVSokHzzzTdBN4Uo6/zyyy+SL18++fnnn4NuClEMBllEIbuAtPr63//+5/pzrl27Vl3kL1y40PXHpuQ9/fTT8uGHH0q2SrfX/8QTT0j9+vWlUaNGObd98MEHcv3110u1atWkSJEictJJJ8n9998v//77r+ljTJo0SU477TQVrB177LHSu3dvOXDgQMx91q1bJw8//LA0adJEihYtqo4LX3zxhenjHTp0SEaOHCl169aVo446SsqXLy+XXnqpfPvtt7ZfF9p6++23S9myZeXII49Uz/vDDz/kut+ePXukX79+UrNmTfVaK1WqJK1atZLFixfbep5ff/1VunbtKg0bNlSv3yqQ/vvvv+WZZ56Rxo0bqzaVKFFCzj77bJkwYUKgx7ulS5dK9+7d1XuN7XL00UfL5ZdfLt9//73p/f/66y+57rrrVPuLFSsmV199taxYsSLX/UaMGKHeR+wPeE9uvvlmyzbMnz9frrjiCqlQoYLa3nXq1JEXXnhBDh48aPt1vPrqq3LyySerbXDiiSfK0KFDTe/3+eefq32hTJky6jWcddZZMnbsWE/eK8D2bdCggdoH8XzYT2bOnJnze+x3eIxevXrZfq1EvogQUSi8/vrrEXwkn3jiicjYsWNzfW3atMn15/zuu+/Uc+K50+X9WblyZUY97/79+yO7d++Oue3II4+M3HTTTZFsZfX6Dxw4oN6rQ4cORcJi48aN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McjKEkwXJCIiIiLyR1oFWV999ZVceeWVUrFiRcmTJ498+OGHCf/miy++kNNOO00KFiwoJ5xwgrzxxhu+tJWIiIiIiLJTWgVZO3fulFNPPVWGDx9u6/4rV66Uyy+/XJo0aSILFy6U++67Tzp06CDTpk2TbMORLCIiIiIif+STNHLppZeqL7tGjhwpVatWleeee079fPLJJ8vXX38tgwcPlmbNmkk2iUSCbgERERERUXZIq5Esp+bMmSNNmzaNuQ3BFW63snfvXtm2bVvMFxERERERkV0ZHWStX79eypcvH3MbfkbgtHv3btO/6devnxQvXjznq3LlypIJmC5IREREROSPjA6yktGjRw/ZunVrztfq1auDbhIREREREaWRtJqT5VSFChVkw4YNMbfh52LFiknhwoVN/wZVCPFFRERERESUjIweyWrQoIHMmDEj5rbp06er27MN0wWJiIiIiPyRVkHWjh07VCl2fGkl2vH9qlWrclL92rdvn3P/O++8U1asWCHdu3eXpUuXyosvvijvvPOOdO3aVbLNjh1Bt4CIiIiIKDukVZD1/fffS7169dQXdOvWTX3fq1cv9fO6detyAi5A+faPP/5YjV5hfS2Uch81alTWlW+HRx8NugVERERERNkhTyTCFZTiQSVCVBlEEQzM5cqEdEFucSIiIiIi72KDtBrJIiIiIiIiCjsGWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELmKQRURERERE5CIGWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELmKQRURERERE5CIGWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELmKQRURERERE5CIGWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELmKQRURERERE5CIGWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELmKQRURERERE5CIGWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELmKQRURERERE5CIGWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELmKQRURERERE5CIGWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELmKQRURERERE5CIGWURERERERC5ikEVEREREROQiBllERERERBSYAwdEFi8WiUQkYzDIIiIiIiKiwNxwg0itWiIvvCAZg0EWEREREREF5p13ov/37y8Zg0EWEREREREFLsJ0QSIiIiIiIjLDIIuIiIiIiAIXyaCRrHx27tStWzfHD9yzZ08pVapUMm0iIiIiIiJKW3kikcQxY968eaVBgwZSoEABWw/69ddfy6+//irVqlWTdLdt2zYpXry4bN26VYoVKybpKk+ezOwlICIiIqL0lue/69SyZUU2bpSMiA1sjWTBxIkTpVy5crbuW7RoUbsPS0RERERElH1zsl5//XUVsdn10ksvSfny5VNpFxERERERZZFIJMvSBbMZ0wWJiIiIiLy/Ti1TRmTTJsmI2MBxdcHVq1fLmjVrcn6eN2+e3HffffLyyy8n31oiIiIiIqIM4TjIatu2rcyaNUt9v379ernoootUoPXoo4/KE088IV4bPny4VKlSRQoVKiT169dXz23ljTfekDx58sR84e+IiIiIiIhCE2T9/PPPctZZZ6nv33nnHalVq5Z8++238uabb6qgxksTJkxQ5eR79+4tP/zwg5x66qnSrFkz2RinDAmG8datW5fz9eeff3raRiIiIiIici6TprQ4DrL2798vBQsWVN9//vnnctVVV6nva9SooYIYLw0aNEg6duwot9xyi9SsWVNGjhwpRYoUkddee83ybzB6VaFChZwvFuQgIiIiIqJQBVmnnHKKCm5mz54t06dPl0suuUTdvnbtWildurR4Zd++fTJ//nxp2rRpzPpd+HnOnDmWf7djxw457rjjpHLlynL11VfL4sWL4z7P3r171YQ2/RcREREREXkrks0jWQMGDFAl2s8//3xp06aNStmDSZMm5aQRemHz5s1y8ODBXCNR+Blzw8ycdNJJapTro48+knHjxsmhQ4ekYcOGMYU7jPr166cqhmhfCM7CLJN2RiIiIiLKXpFsLOG+a9culZoHCHYwwlOyZMmc3//xxx/q93YXLHYKI2WVKlVS878aNGiQc3v37t3lyy+/lLlz59pKdTz55JNVcNi3b1/LkSx8afA6EWiFsYT7ww+LjB0rsmCBSKK3nSXciYiIiCiM8vx3nYrQYssWya4S7mXKlJErrrhClWrHqJI+wAJU/PMqwNKe/4gjjpANGzbE3I6fMdfKjvz580u9evVk+fLllvfBfDO8YfqvsBowAMGnyHPPBd0SIiIiIqLURDJoIMB2kLV06VJVyQ8VBTHHCeXTn3rqKVm0aJH4oUCBAnL66afLjBkzcm5D+h9+1o9sxYMROLT36KOP9rClRERERESUzWwHWccee6zcc889qqIgRo+wADEClnPPPVeqVaumfp45c6YKZLyC8u2vvPKKjB49WpYsWSKdOnWSnTt3qmqD0L59e+nRo0fO/bFu12effSYrVqxQJd/btWunSrh36NBBMtm+fSKjR2Ph6KBbQkRERESUfSNZ+ZL5I+QhYl4TvjDPCYsTT548WQU727dvl6FDh8oNN9zgemOvv/562bRpk/Tq1UsVu6hbt65MnTo1pxjGqlWrVMVBzT///KNKvuO+SG/ESBjmdKH8eyZ75hmRnj1FMIVu586gW0NERERElF1sF76wa8GCBXLgwAE588wzJRPYndwW5CTB7t2j87M0550n8tVX0e+1rasvfLFqlUjIiyYSERERUZbI8991avHiIv/+KxkRGyQ1krVnzx756aefZOPGjWpelH7h3yuvvDK5FpNvOnUSmTIl6FYQEREREWUmx0EW0vMw9wkVBo0QZHk5J4vM7d4t8sUXIuecI5LPxhb9+28/WkVERERElJ1zshwvRoziF61atZJ169apUSz9FwOsYAwdKtKkiUjv3kG3hIiIiIiIHAdZqCyIKn9asQkKj2HDMq8XgIiIiIiyQySSxUFWy5Yt5QvkplHobNtmfvv+/X63hIiIiIgoezmekzVs2DCVLjh79mypXbu25M+fP+b3Xbp0cbN95NCyZfaDLyIiIiKisIhEsjjIeuutt9QCv4UKFVIjWih2ocH3DLKC9ddfie+jL+dOREREREQBpws++uij0qdPH1Ub/o8//pCVK1fmfK1YscLl5hERERERUbaNZH31lUj9+iLz50t2jGTt27dPrr/+esmb13F8Rj644w6RdesO/7xnj8ikSUG2iIiIiIjImfPOi/5/4YXhX6DYjONI6aabbpIJEyZ40xpK2W+/iezYcfjn++8XufXWIFtERERERJScrVslO0aysBbWwIEDZdq0aVKnTp1chS8GDRrkZvsoRaNGBd0CIiIiIqLEsrrwxaJFi6RevXrq+59//jnmd/oiGBRe3ExEREREFDYHD0r2BlmzZs3ypiXkm0zqJSAiIiKizLBvn2QMVq8gIiIiIqLARSJZFmQ1b95ctjlY0faGG26QjRs3ptIu8nBn1VcfJCIiIiIid+WJRBLHjEcccYQsW7ZMypYtm/AB8XCVK1eWhQsXSrVq1STdIbgsXry4WhesWLFikm5zq1CXZP/+zO4pICIiIqL0lSdP7mtUs9vSKTawNScLgVP16tXdbB+lADvagw8G3QoiIiIiIko6yEqm2EWlSpUc/w3ZgxWwn3su6FYQEREREblv9WrJjiDrPG3JZQqFLVvs35fl2omIiIgoXfzxh0jVqpL2WF0ww2VSKUwiIiIiymyzMmS1KAZZRERERERELmKQlYbCVGGFiIiIiMhL27dL2mGQlWbBlVk5diIiIiKiTPXEE5L5QVbv3r3lzz//9KY1FFeLFiIlS4r880/QLSEiIiIi8scvv0jmB1kfffSRHH/88XLhhRfK+PHjZe/evd60jHKZOFFk506RCROCbgkRYWSZqbtERETeW79eMj/IWrhwoXz33XdyyimnyL333isVKlSQTp06qdvIH7ywIwr+M3jBBSIXXsjPIxEREbk0J6tevXrywgsvyNq1a+XVV1+VNWvWSKNGjaROnTry/PPPy9atW5N5WCKitLB5s8gXX0TLzG7cGHRriIiIMscXX2TGuq8pFb6IRCKyf/9+2bdvn/q+ZMmSMmzYMKlcubJMYE6bZ5ihSRSsvLojJ6eoEhERuWfMGMkISQVZ8+fPl86dO8vRRx8tXbt2VSNbS5YskS+//FJ+++03eeqpp6RLly7ut5aU2bODbgFRdjviiMPf168vMm1akK0hIiKitA+yateuLWeffbasXLlSpQquXr1a+vfvLyeccELOfdq0aSObNm1yu61ERKFgnIeVjqVliYiIyDv5nP7BddddJ7feeqtUqlTJ8j5lypSRQ4cOpdo2IqJQMh7evv0WlVdFrrgidpSLiIiIspPjkSxt7pXR7t275Ql25xJRFjCrKHjNNSL5HHdbERERUSZyHGT16dNHduzYkev2Xbt2qd8REWW6kSOtf8dCGERERO7KiuqCGMnKY/JKf/zxRylVqpRb7SIiCqV160Qee8z697t3c+0sIiKibGc7uQUpggiu8FW9evWYQOvgwYNqdOvOO+/0qp1ERKGwbVv83zduLIK6P//8I1KihF+tIiIiorQMsoYMGaJGsVD0AmmBxYsXz/ldgQIFpEqVKtKgQQOv2klElBa0wqoXXojlLoJuDREREYU6yLrpppvU/1WrVpWGDRtK/vz5vWwXEVEobd1q734//OB1S4iIiCitg6xt27ZJsWLF1PdYeBiVBPFlRrsfEVEmevvtoFtARETkrQMHUNQO1/USCnnyZGiQhflY69atk3LlykmJEiVMC19oBTEwP4uIKFMtXGj/vps3Y91AL1tDRESU/JqPQ4aInHOOyFlnHb79119FatSIfr9+vUj58oE1Ma3ZCrJmzpyZUzkQ35sFWURE2WDWrNifUUnQ6pCI+7Zq5UuziMhn6FP+/XeRE09Mz152oiefFOndO/q9viquFmDBtGki7dtL4H75RTIzyDrvvPNyvj///PO9bA8RUWjhZOPEv/961RIiCtrtt4u89prI8OEid90VdGuInNMCrKC9+mri++zcKZm/Ttbrr78u7777bq7bcdvo0aPdahcRUehccon57bNnm99eoYKnzSGiACHAgscfD7olRN7xY93HDh0kIzkOsvr16ydlTCYZYL7W008/7Va7iIhCr2XL6P/IZzfz119cmJgoG+a1EGUqnsN8DLJWrVqlyrgbHXfccep3RETZ1pMNZcvm/n2nTiLjx/vaJCLy+cKT9b4ok7z5ZuzPGzYE1ZIsDLIwYvXTTz/luv3HH3+U0qVLu9UuIqJQLjKsV7To4e+bNjX/u1de8a5NRBQMjFJrOPeSMkm7drE/P/xwUC3JwiCrTZs20qVLF5k1a5Yq144vVBy89957pXXr1t60kogoYCNHxv/9ZZf51RIiCtottwTdAqLUfPtt0C3IfHkiWODKgX379smNN96oCl3kyxctTnjo0CFp3769jBw5UgoUKCCZBAsxFy9eXLZu3Rr4Qstulohlji1R6p8//ecI8zJQ+2fiRJHJk2Pvt2OHyJFHet9GIgrmeMBzKqWb226LTXnHPowy6aeckvu+Xu/feWxe3/74o0idOpI2sYGtEu56CKImTJggffv2VSmChQsXltq1a6s5WURE2aJv39if8+aN9m7jy3jCGDtW5M47fW0eEflo2zaRgPthiVKycqV5gJVoPcig0/bDzHGQpalevbqciBX4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QlAtyZIgC6NXjz76qGzZskXNbfrf//4nmzZtUql4maBgwYKq5r3+i4iy1+7d7o1aIcjauTP+XKWKFTH3VQKDQE/Ts6c3I4OYFxBWs2blvu2YY0S++SY60mesnRTmUbmww3uHoPXDD9EBKqGWiUEW911yKshOwKwIssaNGye7du2SAgUKqHlRZ511lhzlwxLQZcqUkSOOOEI2bNgQczt+xjwxM7jdyf2JiNySL595itFll9krQhEUrw/nPXpIqP32W+zPN9yA1HPr0vYNGvh3QYziEd27h3800Crw0F/U3367SM2akjb0U7lfeSX5BV3DNKcMHToLFwbdEqLM5TjI6tq1q5QrV06lDKIcOlL4/ICg7vTTT5cZM2bk3Hbo0CH1cwOLsxxu198fpk+fbnl/IiK3WPVqV62a+G+DvHDz+rnN5jy9+aaEFkqPYyQrqPXENIsWRdvyzDPRUuFHHundnDk36VZ6yYHLBgQqS5fG3o7X89ZbErp18oYOFXn00djAO9116BBdMP2aa6LBL9I10QHEdLD4Lrggeoz0cyH5IATZ0ZfVQda6devk7bffVmmD1113nRx99NFy9913q3LpXkP5dqzPNXr0aFmyZIkqtLFz505VbRDat2+v5lRpUKBj6tSp8txzz8nSpUvl8ccfl++//146d+7seVuJKLulkjqkTaoPg+uuc/fx/vor923t2kna8mskQJ+uNnBg+Ks0xktHs1pS87zzRNq2DdcF7MMPi3TpYt0Rka4jWZo//4web5Cu+emn0epzZL0/a+nEfi13EZTPPz/8vb6DgZxxfCrPly+fXHHFFaqiINanGjx4sPzxxx/SpEkTOf7448VL119/vTz77LPSq1cvqVu3rixcuFAFUVpxi1WrVqkgUNOwYUMZP368vPzyy2pNrffee08+/PBDqVWrlqftJCLSUoqSORkbspwDnXvx7rvRkQc3RwYyiZa+h7W1vJyTg1Eso7CvMGK13yV6n8I072Pw4Ny3pXtgRcmxs7ZUpihcOLajgZJjMmvAPqxRhZLo//zzj/z5559qdMlrGIWyGon64osvct3WqlUr9UWxduyIzr1AZSdUFDvppKBbRBQ+hw6lVkACF8EozW4cqUH60ejRsQsde1XREP74w16gYGXLFpGyZTP/QiXZhaHXrhWpVMm7YgIoo2xW9dBYIEU7rof186OVu0/0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ISIiIiLKbGkzJ2vVqlWycOFC9f/BgwfV9/jasWNHzn1q1KghEydOVN8j1fC+++6TJ598UiZNmiSLFi2S9u3bS8WKFeWaa66RbFO0KIqBiKxZI5I3bbY6EREREVH6SZspq1hUePTo0Tk/16tXT/0/a9YsOf/889X3v/76q5pHpenevbvs3LlTbr/9dvn333/lnHPOkalTp0qhQoUkG1WpEnQLiIiIiIgyX54IFpQiS0gxRJVBBG/FihULujlERERERBTy2ICJY0RERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELmKQRURERERE5CIGWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZRERERERELsrn5oNlokgkov7ftm1b0E0hIiIiIqIAaTGBFiNYYZCVwPbt29X/lStXDropREREREQUkhihePHilr/PE0kUhmW5Q4cOydq1a6Vo0aKSJ08eyYboHAHl6tWrpVixYkE3J6txW4QHt0V4cFuEC7dHeHBbhAe3RXh4sS0QOiHAqlixouTNaz3ziiNZCeDNO+aYYyTbYEfkgSEcuC3Cg9siPLgtwoXbIzy4LcKD2yJzt0W8ESwNC18QERERERG5iEEWERERERGRixhkUYyCBQtK79691f8ULG6L8OC2CA9ui3Dh9ggPbovw4LYIjyC3BQtfEBERERERuYgjWURERERERC5ikEVEREREROQiBllEREREREQuYpBFRERERETkIgZZREREREQ2aTXjWDsueLt375bt27dLGDHIIgrIt99+G3QT6D+vvfaaTJ48OehmEIUOLyLDYdGiRbJt27agm0Ei8tRTT8mLL76ovs+TJ0/Qzclqjz/+uNSrV0/WrFkjYcQgK0t88MEHcsstt0ivXr14cR+wcePGSZkyZWTo0KGyZ8+eoJsj2b4typcvLx06dJA///xT3caLymC8++670rFjR3n++efVBaWG28N/77//vpx00kmyZMkSdRHJbRCct956S04++WS54YYbpG7duvLCCy8E3aSsPl+ULl1axowZI+ecc07Qzclq4/67jnrllVdk2bJlsnTpUnV72I5VDLIy3G+//SaNGzeWe+65RwoUKCCfffaZXH755fLLL78E3bSsgwuW0047Te677z7p2bOnOnkWKlQo6GZl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15s+z8qVK+Xqq6+WCy64QBYuXKgCrs2bN5s+jplXX31VunfvroItBH1B4GLERESU8fLkyRP3971791YXBjgllihRwrd2ERGls02bNqkRLQRTjRs3lq1bt6pRp/Hjx0vLli3VfZYuXSonn3yyzJkzR41M6T3++OPy4YcfqkBK77333pM2bdrI3r17JW/e6JgQAjcEXrgtf/78lm1CgHbKKafIunXrpFmzZmrUrG3btnGfc8iQIepLGyk7cOCAdOvWTcaMGaNGxDp06CDr169Xrw9/awdHsoiIKOPhZKt94URarFixmNseeOABKV68OAMsIiIHEHRAqVKl1P/z589Xo1tNmzbNuU+NGjXk2GOPVUGWXUglRHD1+uuvy8GDB9XzjB07Vj1uvAAL8DeXX365Oqa3a9dOjWo5NWDAAHnzzTfVY33zzTeybds228GVhkEWERFlvAoVKuR84cSLkS39bZg3YEwXPP/88+Wee+6R++67T0qWLCnly5eXV155RaXF3HLLLWqewAknnCCffvppzHP9/PPPcumll6rHxN/ceOONKs2FiCiTHDp0SB0fGzVqJLVq1VK3YbSnQIECuTqscCzE7+yqWrWqfPbZZ/LII4+oeVt4vDVr1sg777yTsE1vvPGGCq6gdevW8vXXX6vRLSeGDh0qPXr0kGuvvVYFicOGDXPcCccgi4iIyMLo0aOlTJkyMm/ePBVwderUSVq1aiUNGzaUH374Qc1FQBC1a9cudf9///1XzSGoV6+efP/99zJ16lQ14fu6664L+qUQEbkKc7PQqfT222+7/tjr16+Xjh07yk033STfffedSkdE8IYURKR1r1q1SnVkaV9PP/20+jvME0NH2GWXXaZ+xvH7oosuktdee832c2PUDMfts846K+c2pAxidM2JfI7uTURElEVOPfVU6dmzp/oevZr9+/dXJ22c/KFXr14yYsQI+emnn9RcA/R2IsDSTviAk3vlypVl2bJlUr169cBeCxGRWzp37ixTpkyRr776So455pic25EZsG/fPtXhpB/5QdCC39k1fPhwlXUwcODAnNvGjRunjqVz586VM844I2ZOlZauiNTALVu2SOHChWNGt3CM7tOnj0pBxJexJAVSHN3GkSwiIiILderUienJREni2rVrx6TAwMaNG9X/P/74o8yaNSumhxWpJvD777/73n4iIjchOEGANXHiRJk5c6ZK69PDaA/mTM2YMSPnNpR4x8hTgwYNbD8PsgO0ghf6Y7AWNOXLl0+la2tfCLL+/vtv+eijj9TIGgIw7WvBggXyzz//qPRDQGEOjJTpAy19wIbgDsd2jKBpMC8M2QtOcCSLiIjIgnGCNeZy6W/TqhbipA87duyQK6+8Uk2aNjr66KM9by8RkdcpgqgciGAG81K1eVYITDB6hP9vu+02VZkPgQ+KDCHVGgGWvrLg8uXL1fESf7979+6cIKdmzZoqLRCFKwYPHixPPPGEqjK4fft2NT/ruOOOU9kCZlAYAx1hSM82VpRF+iBGuS655BI13xZVETFKhvRDpHVjbi3aqkGbsSYYAjh0lGGOFgK1RJVq9TiSRURE5JLTTjtNFi9eLFWqVInpZcXXkUceGXTziIhSgvRozFlCoIKOI+1rwoQJOfdBcHTFFVeoxYNR1h1pgh988EHM46AkOoKll156SaVS43t8YU0twNxWBHOo6IfbERyhAAYCIn0qoB5Ss1GowiwQQlsmTZqkihChnPyLL76oUhKREo45t6gwq/fQQw+p4K59+/YqQERWAsrBFypUyPZ7xXWyiIgoq6DyFCpiYc6AHqoL4jatTC8uIurWratKvmsQPOFv8aXBCR2pM6hMiAsE/M15552nFsJETy56bJG+MmrUqJx0FyIiSh/IVkBwhlGyvn372vobjmQRERG5pGLFimpNFeTvo/Ig5m8hIMMEcOP8AiIiCqc///xTLdmBUbZFixapyrIoA69f1DgRjmQRERERERH9Z/Xq1WqNLZSoR6iEdcBQXRbpj3YxyCIiIiIiInIRcxeIiIiIiIhcxCCLiIiIiIjIRQyyiIiIiIiIXMQgi4iIiIiIyEUMsoiIiIiIiFzEIIuIiIiIiMhFDLKIiIiIiIhcxCCLiIiIiIjIRQyyiIiIiIiIXMQgi4iIiIiISNzzf3LSwLGZNsQjAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAAi4AAAGzCAYAAAAIWpzfAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAALCFJREFUeJzt3QucjXXix/Efw4xUhpLbJBMlya3cQtYqNbtZsrttsxSyIpFaXhUiklwSUhls6LJbIpYu2JEmtkSryK7KJRSTMuiCPzHMPP/X97evZ/bMmTOXM83tN/N5v17HOM95nvM85zznnOf7/G5POc/zPAMAAOCA8sW9AQAAAHlFcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQpIuXLlzKOPPlooz/3VV1/Z53/xxRcL5fkBl78fKFsILihwOrjqRyq724cffljg6/zmm2/sj+LWrVtznbd79+6mcuXK5vjx49nOc/vtt5vIyEjz3XffmZJq1apVxX4g+OUvf5mxX8uXL2+qVKlirrjiCtO7d2+zZs2akMukpqaap59+2lx99dV2/qpVq5qrrrrKDBw40OzYsSPL/Hv27DF33323qV+/vqlUqZJdpkOHDvY5fvrpp4z53n77bdO/f3/TpEkTExERYWJjY7Pd7m+//dau79JLLzXnnHOOadCggRk+fPjP3t8TJ06074W2IVTwzO42YMCAjHnvvPPOHOc9cOBAvl5zenq6mTp1qn3Neh+bNWtmXn311Szz6Pur70jdunXNueeea5/78ccfN6dOnTLF4fPPP7efc72HgFTgbUBheeyxx+yPZLDLLrusUILL+PHj7Q93ixYtcpxXoeStt94yy5cvN3369Mny+MmTJ80bb7xhfvWrX5kLL7zQlAT16tWzB+mKFStmCi4JCQnFHl4uvvhiM3nyZPv/EydOmN27d5tly5aZl19+2dx22232b+B2//73vzf/+Mc/TM+ePe0B+8yZMzawrFixwrRv3940atQoY96VK1eaP/zhDyYqKsruKx1EFXzWr19vHnzwQfPZZ5+Z5557zs67cOFCs3jxYnPNNdeYOnXqZLu9//d//2fatWtnt3Xw4MH2AP3vf//bzJo1y6xdu9Zs3rzZhrBwff3112bSpEn2YB/soosuMn/729+yTE9MTDSvvPKKuemmmzKmKaR16dIl03y6pNygQYPs5zsmJiZjel5fs4wePdpMmTLFvuetW7e2n/FevXrZMPTHP/4x47Pfr18/c+2119r11ahRw2zcuNGMGzfOJCUlmXfffdfOX9TBRd9theScghnKEF1kEShIL7zwgi7c6X300UdFtk6tS+vUunNz8uRJ7/zzz/fi4uJCPr5w4UL7XIsWLQprG7TMuHHjvKIyZMgQu87i1KlTJ++qq67KMv3s2bPe4MGD7fY99NBDGdM3bdpkp02cODHkMkeOHMm4v3fvXu+8887zGjVq5H3zzTdZ5v/iiy+8mTNnZtw/cOCAl5qaav/ftWtXr169eiG3+ZVXXrHbsGLFikzTx44da6dv2bLFy4/4+Hjv+uuvz/Y9CeWGG27wqlSp4v300085zvf++++HfN/y+pq//vprr2LFivYz40tPT/c6duzoXXzxxfa9l9OnT3sffPBBluXHjx9v179mzRovv/L7/ViyZIlddu3atfleN0oXqopQrKZNm2bPslWyoSL7li1bmqVLl2aZT9UO1113na1WOO+882x1xMMPP2wfW7dunT2DFJ0t+kXq2bUH0Xp+97vf2TPIQ4cOZXlcZ7Hnn3++LS6XH3/80fz5z3+2Z+Y681eJ0RNPPGGL1XPzySefmF//+te2ekPbfcMNN4SsKtM6hg0bZs8otQ6VYqiE4ciRIyHbuKg6QaUtEliNoOODnuOWW27Jsg4V9UdHR9sz+sKmaotnnnnGNG7c2JZkHD16NKPaR1TVE2qZwBIuVWuodGTBggWmdu3aWebXfrj//vsz7qvEIbBkJzvHjh2zf2vWrJlpur8OfT7C9d5779nP7cyZM/O8jKqrVMKjz6KqbnKiz6T2r0pIAuX1Nat0RSVbKmHy6fnuueceW1KkUhVR9ai+j8F++9vf2r/bt2/PdV2nT5+2n2WVMvnfI60j2L59++z26Lus91z7XqVrgVVC+rxrmnTu3Dnjc67vPMouqopQaHSw8g+8Pv3oBB6c1E5BP2yqvlEVwKJFi+wPlaoNunbtaudRdcBvfvMbWyev6icd2FUd8cEHH9jHr7zySjt97Nixtt1Cx44d7fRQP8A+re+ll14yr732mrn33nszpn///fdm9erVthpDP6YqOu/UqZNtV6AD/iWXXGI2bNhgRo0aZQ88OR2otN3aFoWWhx56yB5g/vKXv9gi73/+85+mbdu2dj4dnDWfDgp/+tOfbLG/3rc333zT/uBXr149y3NrW1Q9pkAXWAWh9/eOO+6wB329lgsuuCDjMVWP6aCtx4uCgojex0ceecRW7Wh/qspLVD2i8FKhQvY/QdpetWvJaT/mxy9+8QtbFaTQM336dBsS//Of/9j2KT169MhUVZUXaWlpZujQoeauu+4yTZs2zfNy+qwr/OqzmBMFDn1O9T7kt6pEAVpVWPquBGrTpk3G4zoxyM7Bgwft31CfxWB6H1Q9qJClbVb1kv9dDvTRRx/Z75KqqbQPFFjmzJljvx+qHlI7NO2r++67z4Zgnaj42x/8OlDGFHeRD0pvVVGoW1RUVJZqm0Aq9m7SpIktcvc99dRTdtnDhw8XSFWRqGi8du3aXrt27TJNnzt3rn2e1atX2/sTJkzwzj33XG/Xrl2Z5hs5cqQXERHh7d+/P9ui8B49eniRkZHenj17MqapykPVVL/4xS+yVFEsW7Ysy3aqOF++/PLLLK8vu6qinTt32ulz5szJNL179+5ebGxsxnMWhNyqRZYvX2635emnn854PVpG02rWrOn17NnTS0hI8Pbt25dpuaNHj9p5brnllnxtV07VJjJ//nyvatWqmT6bffv29c6cORP2umbNmuVFR0d7hw4dsvfzWlXUsmVL+xlMS0vLcb633nrLbt/s2bPz/Zr1WP369bNMP3HihH1ufZ5z0qVLF1ul9cMPP+Q439atW+3zqZowUK9evbJ8P4K/+7Jx40Y731//+teMaVQVIRhVRSg0qspQiUDgTY0yAwUWy//www+2lEalD1u2bMmYruohv7g7L9UzeS0N0JmeisgDi6ZVJK8qBFXpyJIlS+z2VKtWzZaC+Dc1ntSZtqoIQtFj6vGhM3iVGgRWR+hMVCUQfpXF3//+d9O8efOM4vhA+WkI2bBhQ1uao1INn0pf9N7r7L4oG1eqekz8Hlxat0q01EtF76l6tQwZMsSWxMTHx9sqM/HfG1U1FAY1cFVpg0rM1EhbPYr0fo0cOTKs51EvJJX0qVRJVSN5tWvXLtsIWJ/B3BoC6zOp0jo1dM4vNexWSWUwv4oqsHdWMDU4fuedd2zDXv+7mB01GBeVkgRSVWuwwO++SpX0Xqr6T+sI/P4DwagqQqHRgaFVq1Y5zqMqIR3E1I1ZdeO+wIOrDmjz58+3RdA6sChUqF3Arbfemq/eHz4dxJ966il7YFAxtKpl3n//ffujq2AjX3zxha1GyO6gFKqNjBw+fNhWM6n+PpiKuRXAkpOTbTdgtftQT5uCpPYxqgJTOwKFAgUwHRzUTTknCjiqsgs8uKhdTH6pGiw4gOgAqh4uuqm6TdVmqjJUdYgO0KpmUPWa5NRlPb9UxaiqR7U18j+fCphap3qvqLpObXPyYsyYMbY6TlVF4fBDZW7VRHr/FNjj4uJ+Vg837cfA75fP7+KcXbse9VjSa1SXa7WHyY0+b/pOqnt5oFDfA4Ul9UZ74YUXbFXsfwst/8tvEwWEQokLio1Cgtq36Kxv9uzZ9mxNpTIqkQj8EdOPqko2dNanA6+ChMLMjTfeaEs28ksNgdWewR/LQn+13sCDiQKG1hNccuTfCjpwFBSdySsE+AdIhQEdpEMdQAIpEKpUyL8FNn7Nj08//TTHLvBah7ZV+/fyyy+34eXs2bM2RKjhqb98QVI7I5WqBYdqfRa1/9XuIi8UatUVW0FX7Y1UcqebwoBCov6vIBiKwrL2hT6DOXn99ddtAM4t4ORG77PaqQR+r0TBUUJ1pdbnWwFY7VPmzp1rCprCntoVqSRJ+10llFqnAlpBlayidKLEBcVGVSQKLao6CCzG1hlYMJ3FqaRFtxkzZtjia52xq1eGqm3yW/2hA4KK+RWGdDDRwdPvoSQ6c9RZb/C4GrlRCY0aF+7cuTPLYxqzRK9HvZT8deTnAJ3Ta1YpgA44Ci56jSplyEuPFzVWVZWdL7exQXKiUKn3VO9DTg0/RSFLja8VBlQVV6tWLVsqomCg6jyNu1JQUlJSQgZehQ1RcMoLlRLoAKvgElw1IhrDSMEv+H3/17/+ZRuXq0F5brT/VN3m93DLL41tpFJLNQAPLE3StviPB2+jqi4V7hQqcmpEHUile3pPVIoYGJJDfQ/UC6tv3772M+dT6POrC31FPW4MSj5KXFBsVB2jH6XAg4jOUnWWGSjUWav/Q+sXf/uDfgX/6OXGP5NVOwVVVwWf2epsUAdOhatgWld2Bzm9Ng0qpmL+wDY0OmjqYK4DuV8dolIbDYCmthbBgs+QA+X2mlU6pd4ZGqjNb9OTG5UAKKT5t7xWmQTTPtXBXAdK/fVfq4LJ/v37s8yv16D3We1e/Go59cTSa1QVod63YDo4qoopP22A9HzBXWr9kjeN6JsXGgxP+yz4puo/9T7T/1XFEkz7X4K7NoeqblQpowKEwt/Poe7xCocq2Qz8bKkkRe19AntuaZ8p9KoHk6pyc+oerhAeuD/V9V/UCyhQqNCsz2Tw5/vZZ5/NEirz+91G6UWJCwqNGoOGGsJdP5JqsKofR5WeaIRa/YirvYga9KpaQSUgPp2ZqirB706r+fQDrC6U/pm8Si3UqE8/xGpPoR87NVANNXJvID2u7VHAkODgooO+uiXr7F9jp+jArhFXt23bZs8YFUqy6yKqtjv++DMar0JnraqmUNhSd+XAdei51A1c7Su0DoU1rVevRw13Q/GrGRQM1AYiOJzo/VKxu9q36ICiUVALg9ojqCpKVK3hj5yrYKHtmTBhQsa8Cmja19oeNXpWyZBKLtQ1XdUtOsD57Yu0T3WQV7Wg2gUFjpyr6hy9Lu0Tnz4zes9E26Dt0j4QvYfdunWz/1fbH5Xq6b6qK/SZUjsbBRdVC/rd1HOj/a62McH8g3Sox3RQVrsRjUwb3A4kmOZTMM6pmiivr1nfFTWQffLJJ23JkkoVdYKg6lqV6vjvudoU6bOkUjd9LjVycSBtc2Dpl/aLhgvwQ6BOKNQFXt9PbYu+WxovSdsWTN8pdeVXGyoFZAVXBbXgtjx6Tm2fxk7Sc6p09vrrry+0zzMckKWfEVCI3aGDu/QuWLDAu/zyy203aY2QqsfUZTLwo5mUlGS7xdapU8d2L9ZfdaMN7qL8xhtveI0bN/YqVKgQVtdodcfV/G3atAn5+PHjx71Ro0Z5l112mV1/9erVvfbt23vTpk3LGLU0u5FBNQqrRujVCLCVK1f2Onfu7G3YsCHLOr777jvv3nvv9WJiYuw6NJqpuuf6I8mG6g6tLt1Dhw71LrroIq9cuXIhu0b7o9dqNODC4Hdt9m96ndqfd9xxh/f2229nmT8lJcWbMmWKXU5dgbWvqlWrZru/L126NOQ6tJ8HDBhgu3LrvVF38g4dOnjPPvusd+rUqTx97vReBtqxY4d36623enXr1rUjyqob8QMPPGC7BxfEe5Jdd+jExES7Pc8880yuz3Pttdd6NWrUyBjVNpRwXrO6XU+aNMm+Vr2P2saXX3450zz+5yyvz6lper2BNArwfffd51144YV2KIFu3bp5ycnJWb4f6lrdr18/+33S50bfE+0XbV/weubNm2e7c2sIArpGo5z+Ke7wBKBwaARTjTyrhpk/t7oBAEoC2rgApZQaOqoKR21oCC0ASgvauACljNoAqa2A2s1oUK+f26W5LFJPMn8MmuyoEbHfNgRA0SG4AKWMehKpQacaL6p3R3BXV+Tt4p8ajC4nX375Zb6vHQQg/8Ju46LeHWqZruGqNXiRuvyFaj0fSC3ONaS2LjqnsSs0EmNgbwAAKEn27t1rbzlRb7HcruoMoASUuKgrqLrZqdumRtnMjc5K1C1z0KBBttudusZpXAaN5KhudwBQ0qi7fuA1pgCUHD+rV5EGD8utxGXEiBF2LIDAkUE1toMGE0pMTMzvqgEAQBlU6G1cNKhQ8HDpKmkJdbVQnwboCrwgmIaQ1oBcGpiI4Z8BAHCDykY0sKEuH/JzLopbpMFF40fogmaBdF+XrdfVQUMNJ60rhubWMA4AALghOTnZjuBcansVjRo1yjbm9WmYZ137Qy/cv+YJAAAo2VRIoU45uhRLQSn04KKrvAZfIE33FUCyu3iXrkUReLVgn5YhuAAA4JaCbOZR6CPn6oJc6kkUSBeeK8jL1AMAgLIh7OCi0SS3bt1qb353Z/3fv7S5qnl0FVefukFrPARdol5XCtZVQ1977TV7DRUAAIBCDS4ff/yxufrqq+1N1BZF/x87dqy9r0Hp/BAjl156qe0OrVIWjf8yffp0M3/+fMZwAQAAYXPi6tBq3BMdHW0b6dLGBQAANxTG8ZurQwMAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAABKd3BJSEgwsbGxplKlSqZt27Zm06ZNOc4/c+ZMc8UVV5hzzjnH1K1b1wwbNsycOnUqv9sMAADKqLCDy+LFi83w4cPNuHHjzJYtW0zz5s1NXFycOXToUMj5Fy5caEaOHGnn3759u1mwYIF9jocffrggth8AAJQhYQeXGTNmmAEDBph+/fqZxo0bm7lz55rKlSub559/PuT8GzZsMB06dDC9evWypTQ33XST6dmzZ66lNAAAAD8ruKSmpprNmzebLl26/O8Jype39zdu3Bhymfbt29tl/KCyd+9es2rVKnPzzTdnu57Tp0+bY8eOZboBAABUCGfmI0eOmLS0NFOzZs1M03V/x44dIZdRSYuWu+6664zneebs2bNm0KBBOVYVTZ482YwfPz6cTQMAAGVAofcqWrdunZk0aZKZPXu2bROzbNkys3LlSjNhwoRslxk1apQ5evRoxi05ObmwNxMAAJS2Epfq1aubiIgIk5KSkmm67teqVSvkMo888ojp3bu3ueuuu+z9pk2bmhMnTpiBAwea0aNH26qmYFFRUfYGAACQ7xKXyMhI07JlS5OUlJQxLT093d5v165dyGVOnjyZJZwo/IiqjgAAAAqlxEXUFbpv376mVatWpk2bNnaMFpWgqJeR9OnTx8TExNh2KtKtWzfbE+nqq6+2Y77s3r3blsJouh9gAAAACiW4xMfHm8OHD5uxY8eagwcPmhYtWpjExMSMBrv79+/PVMIyZswYU65cOfv3wIED5qKLLrKhZeLEieGuGgAAlHHlPAfqa9QdOjo62jbUrVKlSnFvDgAAKKbjN9cqAgAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADgjX8ElISHBxMbGmkqVKpm2bduaTZs25Tj/jz/+aIYMGWJq165toqKiTMOGDc2qVavyu80AAKCMqhDuAosXLzbDhw83c+fOtaFl5syZJi4uzuzcudPUqFEjy/ypqanmxhtvtI8tXbrUxMTEmH379pmqVasW1GsAAABlRDnP87xwFlBYad26tZk1a5a9n56eburWrWuGDh1qRo4cmWV+BZwnn3zS7Nixw1SsWDFfG3ns2DETHR1tjh49aqpUqZKv5wAAAEWrMI7fYVUVqfRk8+bNpkuXLv97gvLl7f2NGzeGXObNN9807dq1s1VFNWvWNE2aNDGTJk0yaWlp2a7n9OnT9sUG3gAAAMIKLkeOHLGBQwEkkO4fPHgw5DJ79+61VURaTu1aHnnkETN9+nTz+OOPZ7ueyZMn24Tm31SiAwAAUOi9ilSVpPYtzz33nGnZsqWJj483o0ePtlVI2Rk1apQtVvJvycnJhb2ZAACgtDXOrV69uomIiDApKSmZput+rVq1Qi6jnkRq26LlfFdeeaUtoVHVU2RkZJZl1PNINwAAgHyXuChkqNQkKSkpU4mK7qsdSygdOnQwu3fvtvP5du3aZQNNqNACAABQYFVF6go9b94889JLL5nt27ebe+65x5w4ccL069fPPt6nTx9b1ePT499//725//77bWBZuXKlbZyrxroAAACFOo6L2qgcPnzYjB071lb3tGjRwiQmJmY02N2/f7/taeRTw9rVq1ebYcOGmWbNmtlxXBRiRowYEe6qAQBAGRf2OC7FgXFcAABwT7GP4wIAAFCcCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAApTu4JCQkmNjYWFOpUiXTtm1bs2nTpjwtt2jRIlOuXDnTo0eP/KwWAACUcWEHl8WLF5vhw4ebcePGmS1btpjmzZubuLg4c+jQoRyX++qrr8wDDzxgOnbs+HO2FwAAlGFhB5cZM2aYAQMGmH79+pnGjRubuXPnmsqVK5vnn38+22XS0tLM7bffbsaPH2/q16+f6zpOnz5tjh07lukGAAAQVnBJTU01mzdvNl26dPnfE5Qvb+9v3Lgx2+Uee+wxU6NGDdO/f/88rWfy5MkmOjo641a3bt1wNhMAAJRSYQWXI0eO2NKTmjVrZpqu+wcPHgy5zPr1682CBQvMvHnz8ryeUaNGmaNHj2bckpOTw9lMAABQSlUozCc/fvy46d27tw0t1atXz/NyUVFR9gYAAJDv4KLwERERYVJSUjJN1/1atWplmX/Pnj22UW63bt0ypqWnp/93xRUqmJ07d5oGDRqEswkAAKAMC6uqKDIy0rRs2dIkJSVlCiK6365duyzzN2rUyGzbts1s3bo149a9e3fTuXNn+3/argAAgEKtKlJX6L59+5pWrVqZNm3amJkzZ5oTJ07YXkbSp08fExMTYxvYapyXJk2aZFq+atWq9m/wdAAAgAIPLvHx8ebw4cNm7NixtkFuixYtTGJiYkaD3f3799ueRgAAAAWtnOd5ninhNI6LukWrh1GVKlWKe3MAAEAxHb8pGgEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAACU7uCSkJBgYmNjTaVKlUzbtm3Npk2bsp133rx5pmPHjqZatWr21qVLlxznBwAAKLDgsnjxYjN8+HAzbtw4s2XLFtO8eXMTFxdnDh06FHL+devWmZ49e5q1a9eajRs3mrp165qbbrrJHDhwINxVAwCAMq6c53leOAuohKV169Zm1qxZ9n56eroNI0OHDjUjR47Mdfm0tDRb8qLl+/TpE3Ke06dP25vv2LFjdh1Hjx41VapUCWdzAQBAMdHxOzo6ukCP32GVuKSmpprNmzfb6p6MJyhf3t5XaUpenDx50pw5c8ZccMEF2c4zefJk+0L9m0ILAABAWMHlyJEjtsSkZs2amabr/sGDB/P0HCNGjDB16tTJFH6CjRo1yqYz/5acnBzOZgIAgFKqQlGubMqUKWbRokW23Ysa9mYnKirK3gAAAPIdXKpXr24iIiJMSkpKpum6X6tWrRyXnTZtmg0u77zzjmnWrFk4qwUAAAi/qigyMtK0bNnSJCUlZUxT41zdb9euXbbLTZ061UyYMMEkJiaaVq1ahbNKAACA/FcVqSt03759bQBp06aNmTlzpjlx4oTp16+ffVw9hWJiYmwDW3niiSfM2LFjzcKFC+3YL35bmPPOO8/eAAAACi24xMfHm8OHD9swohDSokULW5LiN9jdv3+/7WnkmzNnju2NdOutt2Z6Ho0D8+ijj4a7egAAUIaFPY5LaekHDgAASvk4LgAAAMWJ4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAGQQXAADgDIILAABwBsEFAAA4g+ACAACcQXABAADOILgAAABnEFwAAIAzCC4AAMAZBBcAAOAMggsAAHAGwQUAADiD4AIAAJxBcAEAAM4guAAAAGcQXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAACgdAeXhIQEExsbaypVqmTatm1rNm3alOP8S5YsMY0aNbLzN23a1KxatSq/2wsAAMqwsIPL4sWLzfDhw824cePMli1bTPPmzU1cXJw5dOhQyPk3bNhgevbsafr3728++eQT06NHD3v79NNPC2L7AQBAGVLO8zwvnAVUwtK6dWsza9Ysez89Pd3UrVvXDB061IwcOTLL/PHx8ebEiRNmxYoVGdOuvfZa06JFCzN37tw8rfPYsWMmOjraHD161FSpUiWczQUAAMWkMI7fFcKZOTU11WzevNmMGjUqY1r58uVNly5dzMaNG0Muo+kqoQmkEprXX3892/WcPn3a3nx6wf4bAAAA3OAft8MsIym44HLkyBGTlpZmatasmWm67u/YsSPkMgcPHgw5v6ZnZ/LkyWb8+PFZpqtkBwAAuOW7776zJS9FHlyKikp0AktpfvzxR1OvXj2zf//+AnvhyH96VoBMTk6m2q6YsS9KDvZFycL+KDlUY3LJJZeYCy64oMCeM6zgUr16dRMREWFSUlIyTdf9WrVqhVxG08OZX6KiouwtmEILH8KSQfuBfVEysC9KDvZFycL+KDnUrKTAniucmSMjI03Lli1NUlJSxjQ1ztX9du3ahVxG0wPnlzVr1mQ7PwAAQIFVFakKp2/fvqZVq1amTZs2ZubMmbbXUL9+/ezjffr0MTExMbaditx///2mU6dOZvr06aZr165m0aJF5uOPPzbPPfdcuKsGAABlXNjBRd2bDx8+bMaOHWsb2Kpbc2JiYkYDXLVDCSwSat++vVm4cKEZM2aMefjhh83ll19uexQ1adIkz+tUtZHGjQlVfYSixb4oOdgXJQf7omRhf5TufRH2OC4AAADFhWsVAQAAZxBcAACAMwguAADAGQQXAADgDIILAABwRokJLgkJCSY2NtZUqlTJXoF606ZNOc6/ZMkS06hRIzt/06ZNzapVq4psW0u7cPbFvHnzTMeOHU21atXsTRfczG3fofC+Fz6Nl1SuXDnTo0ePQt/GsiLcfaFLlQwZMsTUrl3bdgVt2LAhv1PFtC803tgVV1xhzjnnHHspgGHDhplTp04V2faWVu+9957p1q2bqVOnjv29yeniyb5169aZa665xn4nLrvsMvPiiy+Gv2KvBFi0aJEXGRnpPf/8895nn33mDRgwwKtataqXkpIScv4PPvjAi4iI8KZOnep9/vnn3pgxY7yKFSt627ZtK/JtL23C3Re9evXyEhISvE8++cTbvn27d+edd3rR0dHe119/XeTbXtb3he/LL7/0YmJivI4dO3q33HJLkW1vaRbuvjh9+rTXqlUr7+abb/bWr19v98m6deu8rVu3Fvm2l/V98corr3hRUVH2r/bD6tWrvdq1a3vDhg0r8m0vbVatWuWNHj3aW7ZsmYZV8ZYvX57j/Hv37vUqV67sDR8+3B67n332WXssT0xMDGu9JSK4tGnTxhsyZEjG/bS0NK9OnTre5MmTQ85/2223eV27ds00rW3btt7dd99d6Nta2oW7L4KdPXvWO//8872XXnqpELeybMjPvtD73759e2/+/Ple3759CS7FtC/mzJnj1a9f30tNTS3CrSwbwt0Xmvf666/PNE0Hzg4dOhT6tpYlJg/B5aGHHvKuuuqqTNPi4+O9uLi4sNZV7FVFqampZvPmzbaKwaeRd3V/48aNIZfR9MD5JS4uLtv5UXj7ItjJkyfNmTNnCvRKoGVRfvfFY489ZmrUqGH69+9fRFta+uVnX7z55pv2emyqKtKo4hopfNKkSSYtLa0It7z0yc++0OjtWsavTtq7d6+tsrv55puLbLtRsMfusIf8L2hHjhyxX2b/kgE+3d+xY0fIZXSpgVDzazqKdl8EGzFihK3vDP5wovD3xfr1682CBQvM1q1bi2gry4b87AsdHN99911z++2324Pk7t27zeDBg22o1/DnKLp90atXL7vcddddpxoGc/bsWTNo0CB7CRoUreyO3ceOHTM//fSTbYOUF8Ve4oLSY8qUKbZR6PLly22jORSd48ePm969e9vG0tWrVy/uzSnz0tPTbcmXLibbsmVLe4230aNHm7lz5xb3ppU5agyq0q7Zs2ebLVu2mGXLlpmVK1eaCRMmFPemIZ+KvcRFP7IREREmJSUl03Tdr1WrVshlND2c+VF4+8I3bdo0G1zeeecd06xZs0Le0tIv3H2xZ88e89VXX9kW/oEHT6lQoYLZuXOnadCgQRFseemTn++FehJVrFjRLue78sor7RmnqjsiIyMLfbtLo/zsi0ceecSG+rvuusveVy/UEydOmIEDB9owGXhRYBSu7I7dVapUyXNpixT7HtMXWGckSUlJmX5wdV91xKFoeuD8smbNmmznR+HtC5k6dao9e9FVwlu1alVEW1u6hbsvNDTAtm3bbDWRf+vevbvp3Lmz/b+6gKLovhcdOnSw1UN+eJRdu3bZQENoKdp9oXZ3weHED5RcY7hoFdix2ysh3dvUXe3FF1+0XaQGDhxou7cdPHjQPt67d29v5MiRmbpDV6hQwZs2bZrtgjtu3Di6QxfTvpgyZYrtmrh06VLv22+/zbgdP368GF9F2dwXwehVVHz7Yv/+/bZ33b333uvt3LnTW7FihVejRg3v8ccfL8ZXUTb3hY4P2hevvvqq7Y779ttvew0aNLC9U/Hz6HdeQ2HopjgxY8YM+/99+/bZx7UftD+Cu0M/+OCD9titoTSc7Q4t6s99ySWX2IOgurt9+OGHGY916tTJ/ggHeu2117yGDRva+dW9auXKlcWw1aVTOPuiXr169gMbfNOPBYr+exGI4FK8+2LDhg12mAYdZNU1euLEiba7Oop2X5w5c8Z79NFHbVipVKmSV7duXW/w4MHeDz/8UExbX3qsXbs25O+///7rr/ZH8DItWrSw+07fixdeeCHs9ZbTPwVbGAQAAFA4ir2NCwAAQF4RXAAAgDMILgAAwBkEFwAA4AyCCwAAcAbBBQAAOIPgAgAAnEFwAQAAziC4AAAAZxBcAACAMwguAADAuOL/AVdziaSexrjYAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 5MB\n",
-       "Dimensions:          (time: 20462)\n",
-       "Coordinates:\n",
-       "  * time             (time) datetime64[ns] 164kB 2018-08-12T12:00:00 ... 2018...\n",
-       "Data variables: (12/27)\n",
-       "    Month            (time) int64 164kB 8 8 8 8 8 8 8 8 8 ... 8 8 8 8 8 8 8 8 8\n",
-       "    Day              (time) int64 164kB 12 12 12 12 12 12 ... 26 26 26 26 26 26\n",
-       "    Year             (time) int64 164kB 2018 2018 2018 2018 ... 2018 2018 2018\n",
-       "    Hour             (time) int64 164kB 12 12 12 12 12 12 ... 16 16 16 17 17 17\n",
-       "    Minute           (time) int64 164kB 0 1 2 3 4 5 6 7 ... 55 56 57 58 59 0 1 2\n",
-       "    Second           (time) int64 164kB 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0\n",
-       "    ...               ...\n",
-       "    Pressure_1       (time) float64 164kB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n",
-       "    temperature      (time) float64 164kB 19.95 19.96 19.96 ... 18.77 18.79\n",
-       "    Analog input 1   (time) int64 164kB 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0\n",
-       "    Analog input 2   (time) int64 164kB 15212 15212 15212 ... 15179 15179 15180\n",
-       "    Speed            (time) float64 164kB 0.18 0.288 0.091 ... 0.134 0.105 0.151\n",
-       "    Direction        (time) float64 164kB 38.46 89.2 16.64 ... 264.8 99.28 143.9
" - ], - "text/plain": [ - " Size: 5MB\n", - "Dimensions: (time: 20462)\n", - "Coordinates:\n", - " * time (time) datetime64[ns] 164kB 2018-08-12T12:00:00 ... 2018...\n", - "Data variables: (12/27)\n", - " Month (time) int64 164kB 8 8 8 8 8 8 8 8 8 ... 8 8 8 8 8 8 8 8 8\n", - " Day (time) int64 164kB 12 12 12 12 12 12 ... 26 26 26 26 26 26\n", - " Year (time) int64 164kB 2018 2018 2018 2018 ... 2018 2018 2018\n", - " Hour (time) int64 164kB 12 12 12 12 12 12 ... 16 16 16 17 17 17\n", - " Minute (time) int64 164kB 0 1 2 3 4 5 6 7 ... 55 56 57 58 59 0 1 2\n", - " Second (time) int64 164kB 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0\n", - " ... ...\n", - " Pressure_1 (time) float64 164kB 0.0 0.0 0.0 0.0 ... 0.0 0.0 0.0 0.0\n", - " temperature (time) float64 164kB 19.95 19.96 19.96 ... 18.77 18.79\n", - " Analog input 1 (time) int64 164kB 0 0 0 0 0 0 0 0 0 ... 0 0 0 0 0 0 0 0 0\n", - " Analog input 2 (time) int64 164kB 15212 15212 15212 ... 15179 15179 15180\n", - " Speed (time) float64 164kB 0.18 0.288 0.091 ... 0.134 0.105 0.151\n", - " Direction (time) float64 164kB 38.46 89.2 16.64 ... 264.8 99.28 143.9" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Test reading a Nortek AquaDopp file directly\n", "try:\n", @@ -754,684 +230,10 @@ }, { "cell_type": "code", - "execution_count": 15, + "execution_count": null, "id": "09961d10", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "ADCP file: /Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/moor/raw/msm76_2018/adcp/DS0218_RDI_000_24289.mat\n", - "File exists: True\n", - "\n", - "ADCP Dataset loaded successfully!\n", - "Variables: ['east_velocity', 'north_velocity', 'up_velocity', 'depth', 'pressure', 'ensemble_number', 'heading', 'pitch', 'roll', 'heading_std', 'pitch_std', 'roll_std', 'temperature', 'salinity', 'pressure_std', 'correlation_magnitude', 'echo_intensity', 'status', 'percent_good', 'bt_range', 'bt_velocity', 'bt_correlation', 'bt_amplitude', 'bt_percent_good']\n", - "Time range: 2018-08-15T17:00:06.760004909 to 2019-08-10T18:24:06.759996189\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAAA9wAAAGJCAYAAAB4jDtwAAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAOw9JREFUeJzt3QeYXFXZOPCzpBJIaKGaUBQwH70KQT9BSoAgTRAE1IB8/AFBQR5bUAkRpAgiKFUUFOkgRakJoXcCiMAnhF4MEIKQSgrJ/J/3fM+ss5tNsjvZm5nd/f2eZ7I7d+7eOffeMzfz3vOecxpKpVIpAQAAAO1qifbdHAAAABAE3AAAAFAAATcAAAAUQMANAAAABRBwAwAAQAEE3AAAAFAAATcAAAAUQMANAAAABRBwAwAAQAEE3AAAAFAAATfAYvTHP/4xNTQ0ND569+6d1l133XT00Uen9957r9bF6/AOPvjgJsd36aWXTp/+9KfTvvvum/7yl7+kuXPnzvM3seyyyy5LW221VVp++eVT37598zn55je/mR599NF51v3lL3+Z1lprrXzuNtpoo3TVVVfNs06c5z322CMNHDgwLbXUUmmDDTZIJ598cpoxY8Y873/BBRekr371q2n11VfPZY59mJ8nn3wyffnLX06rrLJK3rd4/9/85jdpzpw5i3ScevXqlff5hBNOaLGMlet27949H6fNN988HXPMMel///d/51n/9ddfz+ueeeaZqa0efvjh9IUvfCH16dMn7+d3v/vdNHXq1HnWmzlzZvrRj36UVltttbTkkkvm8zd69OhF2uaCfPDBB+mMM85IX/ziF9OKK66Yll122bT11luna665ZqF/+4tf/CIfj6gHlaZPn57OO++8NGTIkLTqqqvmurfpppvmOtHSOX3nnXfS//t//y/Xv9jnz3zmM+m4447LZWvu3HPPTf/1X/+Vz+2nPvWpvN60adNSPbnyyivT2WefXetiABSqe7GbB6AlP//5z/OX5ghuHnzwwfwF+7bbbkvPPfdcDgqoXgQYv//97/PvH3/8cXrjjTfS3/72txx0b7fddunmm29O/fr1a1w/gq8Ievbcc8900EEH5YDyxRdfTLfffnsO1iOoKvvJT36STjvttHTYYYelLbfcMm/rwAMPzMHU1772tcYg6pBDDsl/d8QRR6SVVlopPfLII2nEiBFpzJgx6e67787rl51++ulpypQp6XOf+1wOqBYUbG+zzTZpnXXWyYFm1JMoYwS9r7zySjrnnHOqPk6TJk3K+3LSSSflbV1xxRXzrL/TTjvlmxClUimv/8wzz6Q//elP6fzzz8/7EAHdovr73/+edthhhxwonnXWWentt9/OQftLL72U97X5TYPrr78+HXvssfmYxE2OoUOHpnvuuScH19Vsc0HiHMb5j/f46U9/mutJ3MSJ8x43HUaOHNni38X7nXLKKfnGS3Ovvvpq+s53vpPLF8cv6uWdd96Zvv3tb+ebPXF8y+IGweDBg3PQHK/HzZw4BxFYxz5H/Vhiif9rR4n6ETeGos6Xb4r89re/Tc8//3zefj0F3HHNi3MI0GmVAFhsLr300lJcep944okmy4877ri8/Morr5zv306dOrXUkcydO7c0ffr0xfqew4YNKy211FItvnbqqafmY7zffvs1Lnv33XdLDQ0NpcMOO6zF8r/33nuNz99+++1Sjx49SkcddVSTdf77v/+7NGDAgNInn3ySl82cObP00EMPzbO9kSNH5vcfPXp0k+Wvv/563k6Issc+tCTK2LNnz9IHH3zQZPkXv/jFUr9+/UqLepyiDFtvvXU+HnFcKkW5K/e7bOLEiaXBgwfn12+99dbG5a+99lpedsYZZ7SpXLvuumtp1VVXLU2aNKlx2cUXX5y3deeddzYue+yxx+bZ/scff1z6zGc+k8tTzTYX5tVXX83nqvkx23777Uu9evWa7+dz//33z+tsu+22pfXXX7/Ja++//37pueeem+dvDjnkkFy+l156qXHZFVdckZfdcsstTdY94YQT8vKnnnoqPx8/fnype/fupW984xtN1vvtb3+b1/vrX/9aqhe77bZbaY011qh1MQAKJaUcoA5sv/32+edrr73W2HoXKcPR2hgtapFqGq2v5ZTlSMNcf/31c1rzyiuvnA4//PD04YcfNtnm2LFj084775z69++f00+jRf1b3/pWk3WuvvrqnBoc24/WtQ033LBJS+mJJ57YpDW2eWp8pA6XrbnmmjndOVrQtthii/yeF110UX7to48+yq1Y0SoXLatrr712bhVtKcW7KD/+8Y9z6u51112Xxo0b13i8I578/Oc/P8/6sX/ROl0WLcCzZ8/OrYuV6xx55JG5FTNaQEPPnj1zS3Rze++9d/75z3/+s8ny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QMANAABAzTQ0NKTOSsANAAAABRBwAwAAQAEE3AAAAFAAATcAAAA105A6LwE3AAAAFEDADQAAAAUQcAMAAEABBNwAAABQAAE3AAAAFEDADQAAAAUQcAMAAEABBNwAAABQAAE3AAAAdLaA+4ILLkgbbbRR6tevX34MHjw43X777bUsEgAAAHT8gHvAgAHptNNOS08++WQaO3Zs2n777dOee+6Znn/++VoWCwAAABZZ91RDu+++e5Pnv/jFL3Kr96OPPprWX3/9mpULAAAAOnTAXWnOnDnpuuuuS9OmTcup5S2ZOXNmfpRNnjw5/5w9e3Z+dAT1Vs5yeeqtXNQ/dYdqqTu0lrpCtdQdqqXu1MbcOXOaPK/349+W8jWUSqVSqqFnn302B9gzZsxISy+9dLryyivT0KFDW1z3xBNPTCNHjpxnefxNnz59Ur065pH/3Nc4Z/AnNS0LAABAPXn6g4b0x3HdOkzMNH369HTggQemSZMm5bHI6jrgnjVrVnrzzTdzYa+//vr0+9//Pt13331pvfXWa1UL98CBA9PEiRMXuqO1tM7PRjX+/tJJQ1K93Z0ZPXp02mmnnVKPHj1qXRw6EHWHaqk7tJa6QrXUHaql7tTG7c+9m757zT/qNmZqLuLQ/v37tyrgrnlKec+ePdPaa6+df998883TE088kc4555x00UUXzbNur1698qO5+DB0lA9EvZazIx1D6ou6Q7XUHVpLXaFa6g7VUncWr27dmoal9X7s21K+upuHe+7cuU1asQEAAKAjqmkL9/Dhw9Ouu+6aVl999TRlypTcF/vee+9Nd955Zy2LBQAAAB074J4wYUL65je/md555520zDLLpI022igH29FnAgAAADqymgbcf/jDH2r59gAAAFCYuuvDDQAAAJ2BgBsAAAAKIOAGAACAAgi4AQAAoAACbgAAACiAgBsAAAAKIOAGAACAAgi4AQAAqJmGhtRpCbgBAACgAAJuAAAAKICAGwAAAAog4AYAAIACCLgBAACgAAJuAAAAKICAGwAAAAog4AYAAIACCLgBAACgAAJuAAAAKICAGwAAAAog4AYAAIACCLgBAACgAAJuAAAAaqYhdV4CbgAAAKingPuTTz5Jd911V7rooovSlClT8rLx48enqVOntmf5AAAAoEPqXs0fvfHGG2mXXXZJb775Zpo5c2baaaedUt++fdPp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dXHoQNQdqqXu0FrqCtVSd6iWukNrRBzaIQLuaIbv1q1beu+995osj+errLLKPOv36tUrP5qLD4MPxKJxDKmWukO11B1aS12hWuoO1VJ3WJC21I2ajlLes2fPtPnmm6cxY8Y0Lps7d25+Pnjw4FoWDQAAABZJzVPKI0U8BknbYost0uc+97l09tlnp2nTpjWOWg4AAAAdUc0D7v333z+9//776YQTTkjvvvtu2mSTTdIdd9wxz0BqAAAA0JHUPOAORx99dH4AAABAZ1HTPtwAAADQWQm4AQAAoAACbgAAACiAgBsAAAAKIOAGAACAAgi4AQAAoLNOC1atUqmUf06ePLnWRemwZs+enaZPn56PYY8ePWpdHDoQdYdqqTu0lrpCtdQdqqXu0Brl+LMcj3bagHvKlCn558CBA2tdFAAAALqQKVOmpGWWWWaB6zSUWhOW16m5c+em8ePHp759+6aGhoZaF6fD3p2JGxZvvfVW6tevX62LQwei7lAtdYfWUleolrpDtdQdWiNC6Ai2V1tttbTEEkt03hbu2LkBAwbUuhidQlxQXFSohrpDtdQdWktdoVrqDtVSd1iYhbVslxk0DQAAAAog4AYAAIACCLi7uF69eqURI0bkn9AW6g7VUndoLXWFaqk7VEvdob116EHTAAAAoF5p4QYAAIACCLgBAACgAAJuAAAAKICAGwAAAAog4K5Dp556atpyyy1T375900orrZT22muv9OKLLzZZZ8aMGemoo45KK6ywQlp66aXTPvvsk957770m63z3u99Nm2++eR5lcZNNNmnxve6888609dZb5/daccUV83Zef/31hZbxuuuuS4MGDUq9e/dOG264YbrtttuavB5lOfjgg9Nqq62W+vTpk3bZZZf00ksvVXU86Dx15/nnn8/rrbnmmqmhoSGdffbZ86xz//33p9133z3XnVjnpptuqupYUL9159prr82vxbVhjTXWSGeccUaryriw684NN9yQhgwZkssXdefvf/97m48DXaOunHjiifn1pZZaKi233HJpxx13TI899libjwVdr+7Ed5u4vlQ+4jsOxeoMdad5vSk/Wrt9Oi4Bdx2677778gXj0UcfTaNHj06zZ8/OXyKnTZvWuM73vve99Le//S1/uGP98ePHp6985SvzbOtb3/pW2n///Vt8n9deey3tueeeafvtt89fTCOAmjhxYovbqfTwww+nAw44IB166KHp6aefzhe9eDz33HP59Rj4Pp6/+uqr6eabb87rxAUrvtBU7gNdr+5Mnz49ffrTn06nnXZaWmWVVVpcJ8q68cYbp/POO6/N+0/9153bb789HXTQQemII47I14zzzz8//frXv07nnnvuIl13QpT1C1/4Qjr99NMX6VjQ+evKuuuum7fz7LPPpgcffDDfBIx9eP/99xfp2ND5606IAPudd95pfFx11VVVHxO6Tt2prDPxuOSSS3LAHTcG6ORiWjDq24QJE2LqttJ9992Xn3/00UelHj16lK677rrGdf75z3/mdR555JF5/n7EiBGljTfeeJ7l8ffdu3cvzZkzp3HZX//611JDQ0Np1qxZ8y3PfvvtV9ptt92aLNtqq61Khx9+eP79xRdfzGV57rnnGl+P91hxxRVLF198cZv3n85TdyqtscYapV//+tcLXCfKdeONN7Zqe3SMunPAAQeU9t133ybLfvOb35QGDBhQmjt3btXXnUqvvfZaLtfTTz/dyr2lq9aVskmTJuXy3XXXXQvZW7p63Rk2bFhpzz33bOOe0t46Yt1pLurR9ttvv5A9pTPQwt0BTJo0Kf9cfvnl888nn3wy39mLFuOySGFZffXV0yOPPNLq7UZKzRJLLJEuvfTSNGfOnPw+f/7zn/N2e/ToMd+/i/eofO+w8847N773zJkz889IqSmL94n0nWhJoOvWHTqOoupOXB8qrw1hySWXTG+//XZ64403qr7uUDsdva7MmjUr/e53v0vLLLNMzq5h8emodefee+/Nac2f/exn05FHHpk++OCDVpeNrl13yiLV/dZbb80t4nR+Au46N3fu3HTsscemz3/+82mDDTbIy959993Us2fPtOyyyzZZd+WVV86vtdZaa62VRo0alY4//vgcDMf24oISfVcWJN4j3mt+712+wA0fPjx9+OGH+ctMpHjGtiOFhq5bd+gYiqw78QUk+lqPGTMmv8+4cePSr371q/zagq4PC7vuUBsdua7ccsstuZ9nfLmOlNFIU+3fv3+ry0fXrDuRTn7ZZZflbcd3m0hd3nXXXfPNZxaPjlp3Kv3pT3/K/dEX1hWPzkHAXeeiv0r0/7j66qvbfdtxETjssMPSsGHD0hNPPJH/04iL1b777pv7Yb/55pv5y0j5ccopp7Rqu9HCGReruEjFnccYdOKee+7J/yFFqyiLR0esO9SHIutO1Jujjz46ffnLX851Jgbe+9rXvpZfi+uDutOxdOS68qUvfSmPQRF9LyOI2m+//dKECRPafT/oXHUntrPHHnvkQbGij27cuIn/B6PVm8Wjo9adStF/O/qKN29Np3PqXusCMH/xgY8LeYzaPGDAgMblMdhUtBp/9NFHTe7kRXrK/AaiakkMShUpdL/85S8bl11++eVp4MCBebTWLbbYoskov+W0nXiP5qM+Nn/vSDmOv42UnyhrjGK91VZb5W3SdesO9a/ouhMDxESrUHxJiRs3cW2IloQQA+rFiNHVXndYvDp6XYkRytdee+38iC/V66yzTvrDH/6Qs7MoVkevO5Vie5EZ8fLLL6cddtih1WWk69adBx54II+wfs0117Rx7+moNDfWoWghjAvKjTfemO6+++6cvlspgtloRS5fAEJ8cOOu2+DBg1v9PjFidPMW527duuWfkUbTvXv3xi8j8ShfVOI9Kt87RCpeS+8dQVlcrGJKsLFjx+aRrem6dYf6tbjqTmV9+dSnPpVbEGKE39hGXCva47pDsTprXYlrV3kMEorRGetOdKeKPtyrrrpqm8tH16w7cWMvymvMiC6k1qO2Ma8jjzyytMwyy5Tuvffe0jvvvNP4mD59euM6RxxxRGn11Vcv3X333aWxY8eWBg8enB+VXnrppTxKb4yQuO666+bf4zFz5sz8+pgxY/Ko0iNHjiyNGzeu9OSTT5Z23nnnPHp05Xs199BDD+URqs8888w8AmSM9BgjQz777LON61x77bWle+65p/TKK6+UbrrpprzNr3zlK4UcLzpO3Ym/L29r1VVXLX3/+9/Pv8f7lU2ZMqVxnbhEnXXWWfn3N954o5BjxuKtO++//37pggsuyNeOWP7d73631Lt379Jjjz22wPK15rrzwQcf5G3eeuutue5cffXV+XnsB+2no9eVqVOnloYPH55HLn799ddz+Q455JBSr169msyuQfvr6HUn/n+K/7ei7sRsCDGq/WabbVZaZ511SjNmzCjkmNE56k7ljAh9+vTJ70HXIeCuQ/FFsaXHpZde2rjOxx9/XPr2t79dWm655fIHd++9957nS+W2227b4nbiP4myq666qrTpppuWllpqqTxt1x577JEvFAsTAXVcqHr27Flaf/318xfcSuecc06eQiEuNnHx++lPf9p4MaPr1p3ydE3NH/F+ZXGjpqV1YioWOn7diS8zW2+9da43sY0ddtih9Oijj7aqjAu77kRZW3rv+OJD++nodSXKFuVZbbXV8utx8y+uX48//ni7HSM6Z92J4G7IkCH5/7z4fhM3mQ877LDSu+++227HiM5Zd8ouuuii0pJLLpmnMaPraIh/at3KDgAAAJ2NPtwAAABQAAE3AAAAFEDADQAAAAUQcAMAAEABBNwAAABQAAE3AAAAFEDADQAAAAUQcANAJ3XwwQenvfbaq9bFAIAuq3utCwAAtF1DQ8MCXx8xYkQ655xzUqlUWmxlAgCaEnADQAf0zjvvNP5+zTXXpBNOOCG9+OKLjcuWXnrp/AAAakdKOQB0QKusskrjY5lllskt3pXLIthunlK+3Xbbpe985zvp2GOPTcstt1xaeeWV08UXX5ymTZuWDjnkkNS3b9+09tprp9tvv73Jez333HNp1113zduMv/nGN76RJk6cWIO9BoCORcANAF3In/70p9S/f//0+OOP5+D7yCOPTF/96lfTNttsk5566qk0ZMiQHFBPnz49r//RRx+l7bffPm266aZp7Nix6Y477kjvvfde2m+//Wq9KwBQ9wTcANCFbLzxxumnP/1pWmedddLw4cNT7969cwB+2GGH5WWRmv7BBx+kf/zjH3n9c889Nwfbp5xySho0aFD+/ZJLLkn33HNPGjduXK13BwDqmj7cANCFbLTRRo2/d+vWLa2wwgppww03bFwWKeNhwoQJ+eczzzyTg+uW+oO/8sorad11110s5QaAjkjADQBdSI8ePZo8j77flcvKo5/PnTs3/5w6dWrafffd0+mnnz7PtlZdddXCywsAHZmAGwCYr8022yz95S9/SWuuuWbq3t3XBgBoC324AYD5Ouqoo9K///3vdMABB6Qnnngip5HfeeedeVTzOXPm1Lp4AFDXBNwAwHytttpq6aGHHsrBdYxgHv29Y1qxZZddNi2xhK8RALAgDaVSqbTANQAAAIA2c2saAAAACiDgBgAAgAIIuAEAAKAAAm4AAAAogIAbAAAACiDgBgAAgAIIuAEAAKAAAm4AAAAogIAbAAAACiDgBgAAgAIIuAEAAKAAAm4AAABI7e//AzxRR83+D+HOAAAAAElFTkSuQmCC", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "data": { - "text/html": [ - "
\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "\n", - "
<xarray.Dataset> Size: 1GB\n",
-       "Dimensions:                (bin: 40, time: 172829, beam: 4)\n",
-       "Coordinates:\n",
-       "  * time                   (time) datetime64[ns] 1MB 2018-08-15T17:00:06.7600...\n",
-       "  * bin                    (bin) int64 320B 1 2 3 4 5 6 7 ... 35 36 37 38 39 40\n",
-       "    range                  (bin) float64 320B 12.17 20.17 28.17 ... 316.2 324.2\n",
-       "    z                      (bin, time) float64 55MB 11.17 10.17 ... 322.4 323.3\n",
-       "Dimensions without coordinates: beam\n",
-       "Data variables: (12/24)\n",
-       "    east_velocity          (bin, time) float64 55MB 0.002 0.015 ... nan nan\n",
-       "    north_velocity         (bin, time) float64 55MB 0.037 0.024 ... nan nan\n",
-       "    up_velocity            (bin, time) float64 55MB -0.244 -0.293 ... nan nan\n",
-       "    depth                  (time) float64 1MB 1.0 2.0 1.6 1.9 ... 0.8 1.8 0.9\n",
-       "    pressure               (time) float64 1MB 0.923 2.06 0.464 ... 1.183 0.115\n",
-       "    ensemble_number        (time) float64 1MB 1.0 2.0 ... 1.728e+05 1.728e+05\n",
-       "    ...                     ...\n",
-       "    percent_good           (bin, beam, time) float64 221MB 100.0 100.0 ... 0.0\n",
-       "    bt_range               (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n",
-       "    bt_velocity            (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n",
-       "    bt_correlation         (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n",
-       "    bt_amplitude           (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n",
-       "    bt_percent_good        (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n",
-       "Attributes: (12/21)\n",
-       "    Conventions:                            CF-1.8\n",
-       "    title:                                  ADCP (RDI/Teledyne Workhorse) tim...\n",
-       "    source:                                 ADCP MATLAB export (adcp struct)\n",
-       "    instrument_type:                        wh-adcp\n",
-       "    beam_angle:                             20.0\n",
-       "    beam_frequency_kHz:                     150.0\n",
-       "    ...                                     ...\n",
-       "    ranges_definition:                      cell center range from transducer...\n",
-       "    time_coverage_start:                    2018-08-15T17:00:06.760\n",
-       "    time_coverage_end:                      2019-08-10T18:24:06.759\n",
-       "    vertical_velocity_converted_from_cm_s:  false\n",
-       "    pressure_original_units:                Pa\n",
-       "    dropped_zero_mtime_samples:             1
" - ], - "text/plain": [ - " Size: 1GB\n", - "Dimensions: (bin: 40, time: 172829, beam: 4)\n", - "Coordinates:\n", - " * time (time) datetime64[ns] 1MB 2018-08-15T17:00:06.7600...\n", - " * bin (bin) int64 320B 1 2 3 4 5 6 7 ... 35 36 37 38 39 40\n", - " range (bin) float64 320B 12.17 20.17 28.17 ... 316.2 324.2\n", - " z (bin, time) float64 55MB 11.17 10.17 ... 322.4 323.3\n", - "Dimensions without coordinates: beam\n", - "Data variables: (12/24)\n", - " east_velocity (bin, time) float64 55MB 0.002 0.015 ... nan nan\n", - " north_velocity (bin, time) float64 55MB 0.037 0.024 ... nan nan\n", - " up_velocity (bin, time) float64 55MB -0.244 -0.293 ... nan nan\n", - " depth (time) float64 1MB 1.0 2.0 1.6 1.9 ... 0.8 1.8 0.9\n", - " pressure (time) float64 1MB 0.923 2.06 0.464 ... 1.183 0.115\n", - " ensemble_number (time) float64 1MB 1.0 2.0 ... 1.728e+05 1.728e+05\n", - " ... ...\n", - " percent_good (bin, beam, time) float64 221MB 100.0 100.0 ... 0.0\n", - " bt_range (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n", - " bt_velocity (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n", - " bt_correlation (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n", - " bt_amplitude (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n", - " bt_percent_good (beam, time) float64 6MB 0.0 0.0 0.0 ... 0.0 0.0 0.0\n", - "Attributes: (12/21)\n", - " Conventions: CF-1.8\n", - " title: ADCP (RDI/Teledyne Workhorse) tim...\n", - " source: ADCP MATLAB export (adcp struct)\n", - " instrument_type: wh-adcp\n", - " beam_angle: 20.0\n", - " beam_frequency_kHz: 150.0\n", - " ... ...\n", - " ranges_definition: cell center range from transducer...\n", - " time_coverage_start: 2018-08-15T17:00:06.760\n", - " time_coverage_end: 2019-08-10T18:24:06.759\n", - " vertical_velocity_converted_from_cm_s: false\n", - " pressure_original_units: Pa\n", - " dropped_zero_mtime_samples: 1" - ] - }, - "metadata": {}, - "output_type": "display_data" - } - ], + "outputs": [], "source": [ "# Test reading an ADCP MATLAB file directly\n", "try:\n", @@ -1483,45 +285,10 @@ }, { "cell_type": "code", - "execution_count": 16, + "execution_count": null, "id": "00b34a46", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "\n", - "Analyzing successful mooring: dsE_1_2018\n", - "Analyzing processed files in: /Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/moor/proc/dsE_1_2018/microcat\n", - "Found 1 NetCDF files:\n", - "\n", - "📄 Loading: dsE_1_2018_7518_raw.nc\n", - " Variables: ['temperature', 'salinity', 'conductivity', 'pressure', 'serial_number', 'InstrDepth', 'instrument', 'clock_offset', 'start_time', 'end_time']\n", - " Attributes: ['latitude', 'longitude', 'CreateTime', 'DataType', 'mooring_name', 'waterdepth', 'deployment_latitude', 'deployment_longitude', 'deployment_time', 'seabed_latitude', 'seabed_longitude', 'recovery_time']\n", - " Mooring: dsE_1_2018\n", - " Instrument: microcat\n", - " Serial: 7518\n" - ] - }, - { - "data": { - "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAGGCAYAAACqvTJ0AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjMsIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvZiW1igAAAAlwSFlzAAAPYQAAD2EBqD+naQAAfFZJREFUeJzt3QeYE+X2x/Gz9N6bdFSKKCBYUEFBRRAUsVwLlite/lguoFysKIpi712xdxFQ7F3ECiqI2AVBRBHpvZfN//lNnN0km+wm2fT5fp4nm2zK5M3kTTJz5rznzfP5fD4DAAAAAAAAUqhMKp8MAAAAAAAAEIJSAAAAAAAASDmCUgAAAAAAAEg5glIAAAAAAABIOYJSAAAAAAAASDmCUgAAAAAAAEg5glIAAAAAAABIOYJSAAAAAAAASDmCUgAAAAAAAEg5glIAAE+7+uqrLS8vL93NQBK1bNnSBg0alO5mAAAAIARBKQAAYvT77787gaxIp5tuuinqZW3YsMHGjBljRx55pNWpU8d5/JNPPhlXu2bMmGHDhg2zPffc06pWrWrNmze3k046yebOnRv2/j///LPzvNWqVXOe+4wzzrDly5cXud/1119vxxxzjDVs2NBpnwJ5kXzwwQd26KGHWr169axWrVq2//772zPPPBPX60Ghn376yVnv6nvR6NmzZ8T+Wb58+SJBu3D3O/fcc4Pu9/fff9tll13mvL/Vq1d37vPRRx+Fff78/HwbN26c7b333k7/Ut/p27evTZs2zRJFgcbiPofuycsBSa1v9Zs1a9akuykAAIRVLvzVAACgJAMHDrR+/foVub5z585RL2PFihU2duxYJ4DUqVOniDv50bj55pvt888/txNPPNE6duxoS5Yssfvuu8+6dOliX3zxhe21114F9120aJEdcsghVrNmTbvhhhuc4Nhtt91m33//vX311VdWoUKFgvuOHj3aGjVq5Lyud999N+Lzv/baa3bsscfagQceWJCBNnHiRPv3v//tvM7//e9/lg5z5syxMmXKZH1Q6pprrnGCTQoileSKK66w//u//wu6buPGjU6gqXfv3kXur+DRhRdeGHRdmzZtiqxH9bHWrVtbhw4dbPr06RGf/+KLL7Y77rjDTj/9dPvvf//rBEUeeugh69Gjh9NHFawsrXPOOcd69epV8P+CBQvsqquusrPPPtsOPvjggut3220383JQSv1GgTkFiQEAyDQEpQAAiJOCPdrpLo1ddtnFyUBR0GfmzJm23377xb2skSNH2vPPPx8UUDr55JOdAIKyt5599tmC6xWIUpDi66+/dgJiokDBEUcc4WRqacc+cGdfgRAFlurXrx/x+RUA0+v58MMPrWLFigWBg3bt2jnLTFdQym1LaW3ZssVZt9kQ4NL7GMp9/0877bQitzVp0qTEvrzPPvvYypUrnay6F1980Ql+hrNjxw578MEH7V//+ldQlpzuv+uuu9pzzz2XkKCUgp86ufT5UVBK15X2c5mp9JlVFmS6ZUo7AADZL/O3qgAASJDPPvvMCfpUqlTJyZ5Q5kY477//vnXv3t3JLNDQo7Zt29rll1+etICJAlKJcNBBBwUFpERZLRrOp6F6gV566SU7+uijCwJSoqwTZccouylQNJk5sm7dOqtdu3ZQEKhcuXLOUL7KlSsH3fePP/6wX375pcRlKnPMzbhSxoeCJxo6poDH2rVrbevWrTZixAhr0KCB816dddZZznWh7Q8dwqXMHQXJdJva27Rp04KMrsDnfeGFF5xMMT1vlSpVnNcokyZNcoI0el16fQqC/PXXX0Xar9eoIZQK5um+6kvKYnItXLjQySTS9bq9bt26TvAmcJieAnpuAEhD59xhabFm1SlgqUDCgAEDwt6+bds2J9gQida7AlIl2b59u23evNkZshdI75ECeqF9Idm+/PJLZ5iqsgL1HrrZWoHczD4NddV7qfvqPbvyyivN5/PZn3/+6ay3GjVqOJ/X22+/Pejxbn+ZMGGC812h+2hda9irHluaNilL7tRTT3U+W/peku+++87p0wry6ftMz/ef//zHCRoGPl4Za9KqVauCfqO+5Q5BDjdUOHSIbnHtcIOd7mdB/eOUU04J+5oBAAiHTCkAgCdoWJqGLWlHUztZyuZQLafQHecff/zRCdZo+JuG1SlgMW/evCI7jLJp06aCIEYgBbMUjMkE2qFeunSpE5hyKXiybNky23fffYvcXxksb731VlzPpaFlGt6lHfkzzzzT2ZFVIEQZLKGBLgWAPv74Y6d90bjxxhudnV7VNNL7ce+99zq1kRTkWL16tfOeaoiidrK1A66MmUg0VFHDuxSo0468Mt70Pmr4oYY1Ksjkuvbaa51A30UXXeQEu3RZz6HglwKcapfW79133+30kW++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jBkz7Morr7R+/foVCUghtRT4O+CAA+zee++1lStX2kcffZTuJgEAAAAAgBgQlIrSl19+aRUqVLBDDjkk3U1BgEaNGlmHDh3siy++SHdTAAAAAABADAhKxVCkS8PDFJhCZqlXr57z/gAAAAAAgOxBUCpKqhcVroDaoEGDnPpR5557bpHbhg4d6tym+2Sa7du3O0XblWVUtWpVa9y4sf373/+2xYsXF/u4nTt3OkMYW7VqZZUrV7bddtvNrr32Wmf9uCZPnmy9e/e2unXrOq9/9uzZRZYzZ84c69atmzM15HXXXVfk9m+++cZOPPFEa9iwoVNQXrPsDRkyxObOnVvkvnpfAp8fAAAAAABkPoJSCaBpDl944QXbvHlzwXWaCeD555/P2FnhNJPgrFmznACTzhVIUqDomGOOKfZxN998sz344IN233332c8//+z8f8sttzi1nVwbN2607t27O7dFMmzYMDv99NPt1VdfdU7Tpk0ruO2NN95w6kVphr3nnnvOeZ5nn33Watas6bQXAAAAAABkP2bfS4AuXbrY/PnzncDOySef7FynywpIKaModOpKBWsefvhhW7JkibVp08YJtPzrX/8qyETSbHIffvihc7uW8d///tcuuOCCgmUo82rNmjVO4Eezz23bts1OOeUUu+uuu6x8+fJRtVkBnvfffz/oOgWa9t9/f/vjjz8iBtMUPBowYIAdddRRzv8tW7a08ePH21dffVVwnzPOOKOgOHwkq1evtn322cc6duzoZGnp9bjBsrPOOsspJv/yyy8X3F/rsWvXrgX3AwAAAAAA2Y1MqQT5z3/+Y08++WTB/48//rgTXAl144032tNPP23jxo2zH3/80f73v/85GUMff/xxQdBKQ9omTZpkP/30k1111VV2+eWX28SJE4OWM3XqVCcQpvOnnnrKee7A57/66qudgFEs1q5d6wy3q1WrVsT7HHTQQTZlypSCYXTffvutffbZZ9a3b9+Ynmvs2LHWq1cvZxZDDb/r06ePc/27775rK1assEsuuSTs44prGwAAAAAAyB5kSiWIAkujRo2yhQsXWrly5ezzzz93hvR99NFHBffRcLQbbrjBPvjgAzvwwAOd63bddVcnqPPQQw9Zjx49nEyna665JihDaPr06U5Q6qSTTiq4vnbt2k5mU9myZa1du3ZO5pKCRaq75Bb/Vr2naGm4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G2+80a655poi1y9fvty2bNliyV7pa9eudT4cZTwUIt24sZqtXLnFli3bke6mZCWv9huUDv0GsaLPIB70G8SKPoN40G+QrX1mx456tnHjVlu2bL2ZNbImTTbbsmX+xBivW79e6yRBQSllQF188cX24osvWq1atSLeT8Giu+++226//XYngFRap5xySsHlDh06WMeOHW233XZzsqcOP/zwsI8ZNWqUjRw5MihTqlmzZk5GV40aNSzZHwwVY9dzeenLtFq1PKtTp4o1aJDulmQnr/YblA79BrGizyAe9BvEij6DeNBvkK19pnz5PKtSRfvCla1GDZ8NGVLJGjSolLb2ZJJKlSolLig1d+5cK1++fIn3O/DAA53T9u3bLRl23XVXq1evns2bNy9iUEoZWuGytNRRU9FZ9cFI1XNlUnE3/+tOd0uylxf7DUqPfoNY0WcQD/oNYkWfQTzoN8jWPqP94TJl8kyVgw44gP1iV7TvS1T3iiYgVZr7R2vRokW2cuVK22WXXZKyfMSHaS8BAAAAAF7Wv79ZhCpDKEbUMbwPP/zQ2rdvX1A4PJDGcu65555OAfRYbNiwwWbPnu2cZMGCBc7lP/74w7lNQwa/+OIL+/33323KlCk2YMAA23333Z2i6sgszL4HAAAAAPDqvvDrr6e7JTkelLrrrrtsyJAhYesy1axZ08455xy74447YnrymTNnOjP36SSqBaXLV111lVPI/LvvvrNjjjnG2rRpY4MHD7Z99tnHCXwleoY/lA6ZUgAAAAAAr2FfuPSiqikl3377rd18880Rb+/du7fddtttMT15z549nWr5kbz77rsxLQ/pQ6YUAAAAAMCrevVKdwtyPFNq6dKlxdaKKleunC1fvjxR7UIWIToMAAAAAPCyypXT3YIcD0o1adLEfvjhh4i3a6gdBci9i0wpAAAAAIDXsC+coqBUv3797Morr7QtW7YUuW3z5s02ZswYO/roo0vZHGQjMqUAAAAAAF7dF87PN3v77XS3JsdrSo0ePdomT57sFB0fNmyYtW3b1rn+l19+sfvvv9927txpV1xxRTLbCgAAAAAAkFEWLjTbsSPdrcjxoFTDhg1t2rRpdt5559moUaMKCpTn5eVZnz59nMCU7gNvImURAAAAAODFfeHPP093KzwQlJIWLVrYW2+9ZatXr7Z58+Y5ganWrVtb7dq1k9dC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HR0E1sy0mVKHNFsQkEocglJIGoYxIFZNm/rPX321MNChDByNhVfGio7qrFqV1iYigmjSvFVwM/A7QZc//th/+ZhjSvf8CnSpuGey3HFH8paN2KkvSevWiVnemDH+/qgNcwUi4+UWhFXRdQAAksHdljrjjOjur+F4Mn580dtefrnozLKppMxkgKAUkoboMWKh8e1r1wZfp8KN6keqG6MhX24x4XCzZSG91q0reQOqWrXIw/QUiNR9KlSwjKM+6GZ2ITM0aeI/10xDiVTaDfP+/f3n339v5vXvA00S8MQTZk8+me7WAEBuUV0oOeGE6O7vzgx76qnB1z/zjD/bKl00u99hh1nWIvkicQhKIan4sCJamgkkcAhXaN/R1Otu4EM7jpplBNkRgC4piBg4TE9DoPTeZ9JQzeIKiCK9mVKZpkqV4ov8e0XNmv4hkP/5j9k116S7NQCQW4YOTcxyTj89vUkEKtPBgWYIQSkkDcP3EA3NWhX4gzhrVuRi2Zra9u67/Zd/+y017UPpqL5OPBs8JRXuTKWjjvKfk/2ZWa64wjKOZu8TDTf2qtCsSR0JBwAkzuuvmz3wQGyPCay7qTpOmbCPptluFZTK1uxitgsTh6AUkoYPKqKxenXw/507F3//88/39g5fNhk+3Ozzzy3r6UhetMMUkXyvvOI/v+46yzhuQF2TNijg7tUsqVB//52OlgBA7tGEQHLuubE9TnU3TznF7JdfSlc7MZE08kE1GDt2NHv66chlHpD7CEohqTIhCo/M1q5d4eV586J7TL9+/vOff05OmxA9ZbZFcvvtlnNGjUp3C6Bac5lKs0C6li5NZ0syS+PGZvvt5y+2e9VV6W4NAGQvd0KNeA7+q9B527aWMZQp5R5oOvNMs40b2Xf0KoJSSBqG76EkgUGlESNirxOlqeCXL094sxClGTOKn0I4MMMo2910k/881nR5JMcBB1hG0tHn007zX65VyzxlyZLid5JmzvTPFHX99alsFQAgUykoFTrJka4LtWJF8PBD5B6CUkgahu+hODff7A8qyaefmt15Z2yPX7jQf86PVPrsv3/w/0q7/t///EWox42znHLppelugfe89po/kBHIPdDxySeWsbZujbxhncuirfOn+iHawQAAxCc0kJOtov2drF/frEsXyyja1nV/71F65RKwDCAsMqVQnMsuK7zcvXv86ct9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", - "text/plain": [ - "
" - ] - }, - "metadata": {}, - "output_type": "display_data" - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - " 📊 Plot saved: dsE_1_2018_microcat_7518_temperature_raw.png\n" - ] - } - ], + "outputs": [], "source": [ "# Analyze processed files\n", "def analyze_processed_mooring(mooring_name, basedir, instrument_type='microcat'):\n", diff --git a/notebooks/demo_stage2.ipynb b/notebooks/demo_stage2.ipynb index 0a3a6c4..afb8836 100644 --- a/notebooks/demo_stage2.ipynb +++ b/notebooks/demo_stage2.ipynb @@ -35,8 +35,7 @@ " 'ds13_1_2012','ds14_1_2012','ds15_1_2012','ds16_1_2012','ds17_1_2012',\n", " 'ds19_1_2012','ds18_1_2012','ds28_1_2017',\n", " 'dsA_1_2018','dsB_1_2018','dsC_1_2018', 'dsD_1_2018','dsE_1_2018','dsF_1_2018',\n", - " 'dsM1_1_2017','dsM2_1_2017','dsM3_1_2017','dsM4_1_2017','dsM5_1_2017']\n", - "moorlist = ['dsE_1_2018']" + " 'dsM1_1_2017','dsM2_1_2017','dsM3_1_2017','dsM4_1_2017','dsM5_1_2017']" ] }, { diff --git a/notebooks/demo_step1.ipynb b/notebooks/demo_step1.ipynb index 9305a06..0344576 100644 --- a/notebooks/demo_step1.ipynb +++ b/notebooks/demo_step1.ipynb @@ -1,630 +1,630 @@ { - "cells": [ - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "# Step 1: Time Gridding and Optional Filtering Demo\n", - "\n", - "This notebook demonstrates the Step 1 processing workflow for mooring data:\n", - "- Loading multiple instrument datasets\n", - "- Optional time-domain filtering (applied BEFORE interpolation)\n", - "- Interpolating onto a common time grid\n", - "- Combining into a unified mooring dataset\n", - "\n", - "**Key Point**: Filtering is applied to individual instrument records on their native time grids BEFORE interpolation to preserve data integrity.\n", - "\n", - "Version: 1.0 \n", - "Date: 2025-09-07" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import pandas as pd\n", - "import xarray as xr\n", - "import matplotlib.pyplot as plt\n", - "import seaborn as sns\n", - "from pathlib import Path\n", - "import yaml\n", - "\n", - "# Import the time gridding module\n", - "from oceanarray.time_gridding import (\n", - " TimeGriddingProcessor,\n", - " time_gridding_mooring,\n", - " process_multiple_moorings_time_gridding\n", - ")\n", - "\n", - "# Set up plotting\n", - "plt.style.use('default')\n", - "sns.set_palette(\"husl\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 1. Data Setup and Configuration\n", - "\n", - "First, let's set up our data paths and examine the mooring configuration." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Set your data paths here\n", - "basedir = '/Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/'\n", - "mooring_name = 'dsE_1_2018'\n", - "\n", - "# Construct paths\n", - "proc_dir = Path(basedir) / 'moor' / 'proc' / mooring_name\n", - "config_file = proc_dir / f\"{mooring_name}.mooring.yaml\"\n", - "\n", - "print(f\"Processing directory: {proc_dir}\")\n", - "print(f\"Configuration file: {config_file}\")\n", - "print(f\"Config exists: {config_file.exists()}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Load and examine the mooring configuration\n", - "if config_file.exists():\n", - " with open(config_file, 'r') as f:\n", - " config = yaml.safe_load(f)\n", - "\n", - " print(\"Mooring Configuration:\")\n", - " print(f\"Name: {config['name']}\")\n", - " print(f\"Water depth: {config.get('waterdepth', 'unknown')} m\")\n", - " print(f\"Location: {config.get('latitude', 'unknown')}°N, {config.get('longitude', 'unknown')}°E\")\n", - " print(f\"\\nInstruments ({len(config.get('instruments', []))}):\")\n", - "\n", - " for i, inst in enumerate(config.get('instruments', [])):\n", - " print(f\" {i+1}. {inst.get('instrument', 'unknown')} \"\n", - " f\"(serial: {inst.get('serial num.', 'unknown')}) at {inst.get('depth', 'unknown')} m\")\n", - "else:\n", - " print(\"Configuration file not found!\")\n", - " print(\"Please check your data path and mooring name.\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 2. Examine Individual Instrument Files\n", - "\n", - "Let's look at the individual instrument files before processing to understand the different sampling rates and data characteristics." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Find and examine individual instrument files\n", - "file_suffix = \"_use\"\n", - "instrument_files = []\n", - "instrument_datasets = []\n", - "rows = []\n", - "\n", - "if config_file.exists():\n", - " for inst_config in config.get(\"instruments\", []):\n", - " instrument_type = inst_config.get(\"instrument\", \"unknown\")\n", - " serial = inst_config.get(\"serial\", 0)\n", - " depth = inst_config.get(\"depth\", 0)\n", - "\n", - " # Look for the file\n", - " filename = f\"{mooring_name}_{serial}{file_suffix}.nc\"\n", - " filepath = proc_dir / instrument_type / filename\n", - "\n", - " if filepath.exists():\n", - " ds = xr.open_dataset(filepath)\n", - " instrument_files.append(filepath)\n", - " instrument_datasets.append(ds)\n", - "\n", - " # Time coverage\n", - " t0, t1 = ds.time.values[0], ds.time.values[-1]\n", - " npoints = len(ds.time)\n", - "\n", - " # Median sampling interval\n", - " time_diff = np.diff(ds.time.values) / np.timedelta64(1, \"m\") # in minutes\n", - " median_interval = np.nanmedian(time_diff)\n", - " if median_interval > 1:\n", - " sampling = f\"{median_interval:.1f} min\"\n", - " else:\n", - " sampling = f\"{median_interval*60:.1f} sec\"\n", - "\n", - " # Collect a row for the table\n", - " rows.append(\n", - " {\n", - " \"Instrument\": instrument_type,\n", - " \"Serial\": serial,\n", - " \"Depth [m]\": depth,\n", - " \"File\": filepath.name,\n", - " \"Start\": str(t0)[:19],\n", - " \"End\": str(t1)[:19],\n", - " \"Points\": npoints,\n", - " \"Sampling\": sampling,\n", - " \"Variables\": \", \".join(list(ds.data_vars)),\n", - " }\n", - " )\n", - " else:\n", - " rows.append(\n", - " {\n", - " \"Instrument\": instrument_type,\n", - " \"Serial\": serial,\n", - " \"Depth [m]\": depth,\n", - " \"File\": \"MISSING\",\n", - " \"Start\": \"\",\n", - " \"End\": \"\",\n", - " \"Points\": 0,\n", - " \"Sampling\": \"\",\n", - " \"Variables\": \"\",\n", - " }\n", - " )\n", - "\n", - " # Make a DataFrame summary\n", - " summary = pd.DataFrame(rows)\n", - " pd.set_option(\"display.max_colwidth\", 80) # allow long var lists\n", - " print(summary.to_markdown(index=False))\n", - "\n", - " print(f\"\\nFound {len(instrument_datasets)} instrument datasets\")\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 4. Process with Time Gridding (No Filtering)\n", - "\n", - "First, let's process the mooring without any filtering to see the basic time gridding functionality." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Process without filtering\n", - "print(\"Processing mooring with time gridding only (no filtering)...\")\n", - "print(\"=\"*60)\n", - "\n", - "result = time_gridding_mooring(mooring_name, basedir, file_suffix='_use')\n", - "\n", - "print(f\"\\nProcessing result: {'SUCCESS' if result else 'FAILED'}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Load and examine the combined dataset\n", - "output_file = proc_dir / f\"{mooring_name}_mooring_use.nc\"\n", - "\n", - "if output_file.exists():\n", - " print(f\"Output file exists: {output_file}\")\n", - "\n", - " # Load the combined dataset\n", - " combined_ds = xr.open_dataset(output_file)\n", - "else:\n", - " print(\"Output file not found - processing may have failed\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 5. Visualize Combined Dataset\n", - "\n", - "Let's plot the combined dataset to see how the different instruments look on the common time grid." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "import numpy as np\n", - "import matplotlib.pyplot as plt\n", - "\n", - "def plot_combined_timeseries(\n", - " combined_ds,\n", - " variables=(\"temperature\", \"salinity\", \"pressure\"),\n", - " cmap_name=\"viridis\",\n", - " line_alpha=0.8,\n", - " line_width=1.2,\n", - " percentile_limits=(1, 99),\n", - "):\n", - " \"\"\"\n", - " Plot selected variables from a combined mooring dataset as stacked time series.\n", - "\n", - " Parameters\n", - " ----------\n", - " combined_ds : xarray.Dataset\n", - " Must have dims: time, N_LEVELS. Optional coords: nominal_depth, serial_number.\n", - " variables : iterable[str]\n", - " Variable names to try to plot (if present in dataset).\n", - " cmap_name : str\n", - " Matplotlib colormap name for coloring by instrument level.\n", - " line_alpha : float\n", - " Line transparency.\n", - " line_width : float\n", - " Line width.\n", - " percentile_limits : (low, high)\n", - " Percentiles to use for automatic y-limits (e.g., (1, 99)).\n", - " \"\"\"\n", - " if combined_ds is None:\n", - " print(\"Combined dataset not available.\")\n", - " return None, None\n", - " n_levels = combined_ds.sizes.get(\"N_LEVELS\")\n", - " if n_levels is None:\n", - " raise ValueError(\"Dataset must contain dimension 'N_LEVELS'.\")\n", - "\n", - " available = [v for v in variables if v in combined_ds.data_vars]\n", - " if not available:\n", - " print(\"No requested variables found to plot.\")\n", - " return None, None\n", - "\n", - " # Colors by level\n", - " cmap = plt.get_cmap(cmap_name)\n", - " colors = cmap(np.linspace(0, 1, n_levels))\n", - "\n", - " fig, axes = plt.subplots(\n", - " len(available), 1, figsize=(14, 3.6 * len(available)), sharex=True, constrained_layout=True\n", - " )\n", - " if len(available) == 1:\n", - " axes = [axes]\n", - "\n", - " depth_arr = combined_ds.get(\"nominal_depth\")\n", - " serial_arr = combined_ds.get(\"serial_number\")\n", - "\n", - " first_axis = True\n", - " for ax, var in zip(axes, available):\n", - " values_for_limits = []\n", - " for level in range(n_levels):\n", - " depth = None if depth_arr is None else depth_arr.values[level]\n", - " serial = None if serial_arr is None else serial_arr.values[level]\n", - " label = None\n", - " if first_axis:\n", - " if depth is not None and np.isfinite(depth):\n", - " label = f\"Serial {serial} ({int(depth)} m)\" if serial is not None else f\"({int(depth)} m)\"\n", - " elif serial is not None:\n", - " label = f\"Serial {serial}\"\n", - "\n", - " da = combined_ds[var].isel(N_LEVELS=level)\n", - " da = da.where(np.isfinite(da), drop=True)\n", - " if da.size == 0:\n", - " continue\n", - "\n", - " values_for_limits.append(da.values)\n", - "\n", - " ax.plot(\n", - " da[\"time\"].values,\n", - " da.values,\n", - " color=colors[level],\n", - " alpha=line_alpha,\n", - " linewidth=line_width,\n", - " label=label,\n", - " )\n", - "\n", - " # Set labels and grid\n", - " ax.set_ylabel(var.replace(\"_\", \" \").title())\n", - " ax.grid(True, alpha=0.3)\n", - " ax.set_title(f\"{var.replace('_', ' ').title()} — Combined Time Grid\")\n", - "\n", - " # Legend only once\n", - " if first_axis:\n", - " ax.legend(ncol=3, fontsize=8, loc=\"upper right\", frameon=False)\n", - " first_axis = False\n", - "\n", - " # Auto y-limits based on percentiles\n", - " if values_for_limits:\n", - " flat = np.concatenate(values_for_limits)\n", - " low, high = np.nanpercentile(flat, percentile_limits)\n", - " ax.set_ylim(low, high)\n", - "\n", - " axes[-1].set_xlabel(\"Time\")\n", - " return fig, axes\n", - "\n", - "# Usage:\n", - "if 'combined_ds' in locals():\n", - " plot_combined_timeseries(combined_ds)\n" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 6. Process with Low-pass Filtering (RAPID-style)\n", - "\n", - "Now let's apply the RAPID-style 2-day low-pass filter to remove tidal and inertial variability. Remember: **filtering is applied to individual instruments BEFORE interpolation**." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Process with RAPID-style low-pass filtering\n", - "print(\"Processing mooring with 2-day low-pass filtering (RAPID-style)...\")\n", - "print(\"=\"*60)\n", - "print(\"IMPORTANT: Filtering is applied to each instrument on its native time grid\")\n", - "print(\"BEFORE interpolation to preserve data integrity.\")\n", - "print()\n", - "\n", - "filter_params = {\n", - " 'cutoff_days': 2.0, # 2-day cutoff\n", - " 'order': 6 # 6th order Butterworth\n", - "}\n", - "\n", - "result_filtered = time_gridding_mooring(\n", - " mooring_name, basedir,\n", - " file_suffix='_use',\n", - " filter_type='lowpass',\n", - " filter_params=filter_params\n", - ")\n", - "\n", - "print(f\"\\nFiltered processing result: {'SUCCESS' if result_filtered else 'FAILED'}\")" - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Load the filtered dataset\n", - "filtered_output_file = proc_dir / f\"{mooring_name}_mooring_use_lowpass.nc\"\n", - "\n", - "if filtered_output_file.exists():\n", - " print(f\"Filtered output file created: {filtered_output_file}\")\n", - "\n", - " # Load the filtered dataset\n", - " filtered_ds = xr.open_dataset(filtered_output_file)\n", - "\n", - " print(\"\\nFiltered Dataset Attributes:\")\n", - " filter_attrs = {k: v for k, v in filtered_ds.attrs.items()\n", - " if 'filter' in k.lower()}\n", - " for key, value in filter_attrs.items():\n", - " print(f\" {key}: {value}\")\n", - "\n", - " print(f\"\\nDataset shape: {dict(filtered_ds.dims)}\")\n", - "else:\n", - " print(\"Filtered output file not found\")\n", - " filtered_ds = None" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 7. Compare Filtered vs Unfiltered Data\n", - "\n", - "Let's compare the original and filtered data to see the effect of the low-pass filter." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if 'combined_ds' in locals() and filtered_ds is not None:\n", - " # Compare filtered vs unfiltered for a subset of data\n", - " # Select a 10-day window for detailed comparison\n", - " start_time = combined_ds.time.values[len(combined_ds.time)//4] # Start 1/4 through\n", - " end_time = start_time + np.timedelta64(10, 'D') # 10-day window\n", - "\n", - " # Select subset\n", - " subset_orig = combined_ds.sel(time=slice(start_time, end_time))\n", - " subset_filt = filtered_ds.sel(time=slice(start_time, end_time))\n", - "\n", - " # Plot comparison for temperature\n", - " if 'temperature' in subset_orig.data_vars:\n", - " fig, axes = plt.subplots(2, 1, figsize=(14, 8), sharex=True)\n", - "\n", - " # Choose a representative level (first one with data)\n", - " level = 0\n", - " depth = subset_orig.nominal_depth.values[level]\n", - " serial = subset_orig.serial_number.values[level]\n", - "\n", - " # Original data\n", - " orig_temp = subset_orig.temperature.isel(N_LEVELS=level)\n", - " axes[0].plot(orig_temp.time, orig_temp, 'b-', alpha=0.7, linewidth=1,\n", - " label='Original')\n", - " axes[0].set_ylabel('Temperature (°C)')\n", - " axes[0].set_title(f'Original Data - Serial {serial} at {depth}m')\n", - " axes[0].grid(True, alpha=0.3)\n", - " axes[0].legend()\n", - "\n", - " # Filtered data\n", - " filt_temp = subset_filt.temperature.isel(N_LEVELS=level)\n", - " axes[1].plot(filt_temp.time, filt_temp, 'r-', alpha=0.7, linewidth=1.5,\n", - " label='2-day Low-pass Filtered')\n", - " axes[1].set_ylabel('Temperature (°C)')\n", - " axes[1].set_xlabel('Time')\n", - " axes[1].set_title(f'Filtered Data - Serial {serial} at {depth}m')\n", - " axes[1].grid(True, alpha=0.3)\n", - " axes[1].legend()\n", - "\n", - " plt.tight_layout()\n", - " plt.show()\n", - "\n", - " # Overlay comparison\n", - " fig, ax = plt.subplots(1, 1, figsize=(14, 6))\n", - " ax.plot(orig_temp.time, orig_temp, 'b-', alpha=0.5, linewidth=0.8,\n", - " label='Original')\n", - " ax.plot(filt_temp.time, filt_temp, 'r-', alpha=0.8, linewidth=2,\n", - " label='2-day Low-pass Filtered')\n", - " ax.set_ylabel('Temperature (°C)')\n", - " ax.set_xlabel('Time')\n", - " ax.set_title(f'Filtering Comparison - Serial {serial} at {depth}m')\n", - " ax.legend()\n", - " ax.grid(True, alpha=0.3)\n", - " plt.tight_layout()\n", - " plt.show()\n", - "else:\n", - " print(\"Cannot compare - one or both datasets not available\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 8. Spectral Analysis: Effect of Filtering\n", - "\n", - "Let's examine the spectral characteristics to see how the filter affects different frequency components." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "if 'combined_ds' in locals() and filtered_ds is not None:\n", - " from scipy import signal\n", - "\n", - " # Select a level with good data coverage\n", - " level = 0\n", - "\n", - " # Get temperature data\n", - " if 'temperature' in combined_ds.data_vars:\n", - " orig_temp = combined_ds.temperature.isel(N_LEVELS=level).dropna('time')\n", - " filt_temp = filtered_ds.temperature.isel(N_LEVELS=level).dropna('time')\n", - "\n", - " if len(orig_temp) > 100: # Ensure sufficient data\n", - " # Calculate sampling rate\n", - " dt_hours = float(np.median(np.diff(orig_temp.time.values)) / np.timedelta64(1, 'h'))\n", - " fs = 1.0 / dt_hours # samples per hour\n", - "\n", - " # Compute power spectral density\n", - " f_orig, psd_orig = signal.welch(orig_temp.values, fs=fs, nperseg=min(256, len(orig_temp)//4))\n", - " f_filt, psd_filt = signal.welch(filt_temp.values, fs=fs, nperseg=min(256, len(filt_temp)//4))\n", - "\n", - " # Convert frequency to period in days\n", - " period_orig = 1.0 / (f_orig * 24) # days\n", - " period_filt = 1.0 / (f_filt * 24) # days\n", - "\n", - " # Plot power spectral density\n", - " fig, ax = plt.subplots(1, 1, figsize=(12, 6))\n", - "\n", - " ax.loglog(period_orig[1:], psd_orig[1:], 'b-', alpha=0.7,\n", - " label='Original', linewidth=1.5)\n", - " ax.loglog(period_filt[1:], psd_filt[1:], 'r-', alpha=0.8,\n", - " label='2-day Low-pass Filtered', linewidth=2)\n", - "\n", - " # Mark important periods\n", - " ax.axvline(2.0, color='gray', linestyle='--', alpha=0.7,\n", - " label='2-day cutoff')\n", - " ax.axvline(1.0, color='gray', linestyle=':', alpha=0.7,\n", - " label='1-day (diurnal)')\n", - " ax.axvline(0.5, color='gray', linestyle=':', alpha=0.7,\n", - " label='12-hour (semidiurnal)')\n", - "\n", - " ax.set_xlabel('Period (days)')\n", - " ax.set_ylabel('Power Spectral Density')\n", - " ax.set_title('Spectral Analysis: Effect of 2-day Low-pass Filter')\n", - " ax.legend()\n", - " ax.grid(True, alpha=0.3)\n", - " ax.set_xlim(0.1, 100)\n", - "\n", - " plt.tight_layout()\n", - " plt.show()\n", - "\n", - " print(f\"Spectral analysis completed for level {level}\")\n", - " print(f\"Sampling rate: {dt_hours:.2f} hours\")\n", - " print(f\"Data length: {len(orig_temp)} points\")\n", - " else:\n", - " print(\"Insufficient data for spectral analysis\")\n", - " else:\n", - " print(\"Temperature data not available for spectral analysis\")\n", - "else:\n", - " print(\"Cannot perform spectral analysis - datasets not available\")" - ] - }, - { - "cell_type": "markdown", - "metadata": {}, - "source": [ - "## 10. Multiple Mooring Processing Example\n", - "\n", - "The time gridding module also supports batch processing of multiple moorings." - ] - }, - { - "cell_type": "code", - "execution_count": null, - "metadata": {}, - "outputs": [], - "source": [ - "# Base directory containing the mooring data\n", - "basedir = '/Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/'\n", - "\n", - "# Example of processing multiple moorings\n", - "# (This will only work if you have multiple moorings in your dataset)\n", - "\n", - "# List available moorings\n", - "moor_base = Path(basedir) / 'moor' / 'proc'\n", - "available_moorings = [d.name for d in moor_base.iterdir() if d.is_dir()]\n", - "\n", - "print(f\"Available moorings in {moor_base}:\")\n", - "for mooring in available_moorings[:5]: # Show first 5\n", - " print(f\" - {mooring}\")\n", - "\n", - "if len(available_moorings) > 5:\n", - " print(f\" ... and {len(available_moorings)-5} more\")\n", - "\n", - "# Example batch processing (commented out to avoid running on all moorings)\n", - "moorings_to_process = ['dsE_1_2018'] # Add your mooring names\n", - "\n", - "results = process_multiple_moorings_time_gridding(\n", - " moorings_to_process,\n", - " basedir,\n", - ")\n", - "\n", - "print(\"Batch processing results:\")\n", - "for mooring, success in results.items():\n", - " status = \"SUCCESS\" if success else \"FAILED\"\n", - " print(f\" {mooring}: {status}\")" - ] - } - ], - "metadata": { - "kernelspec": { - "display_name": "venv (3.11.7)", - "language": "python", - "name": "python3" - }, - "language_info": { - "codemirror_mode": { - "name": "ipython", - "version": 3 - }, - "file_extension": ".py", - "mimetype": "text/x-python", - "name": "python", - "nbconvert_exporter": "python", - "pygments_lexer": "ipython3", - "version": "3.11.7" - } + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Step 1: Time Gridding and Optional Filtering Demo\n", + "\n", + "This notebook demonstrates the Step 1 processing workflow for mooring data:\n", + "- Loading multiple instrument datasets\n", + "- Optional time-domain filtering (applied BEFORE interpolation)\n", + "- Interpolating onto a common time grid\n", + "- Combining into a unified mooring dataset\n", + "\n", + "**Key Point**: Filtering is applied to individual instrument records on their native time grids BEFORE interpolation to preserve data integrity.\n", + "\n", + "Version: 1.0 \n", + "Date: 2025-09-07" + ] }, - "nbformat": 4, - "nbformat_minor": 4 + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import pandas as pd\n", + "import xarray as xr\n", + "import matplotlib.pyplot as plt\n", + "import seaborn as sns\n", + "from pathlib import Path\n", + "import yaml\n", + "\n", + "# Import the time gridding module\n", + "from oceanarray.time_gridding import (\n", + " TimeGriddingProcessor,\n", + " time_gridding_mooring,\n", + " process_multiple_moorings_time_gridding\n", + ")\n", + "\n", + "# Set up plotting\n", + "plt.style.use('default')\n", + "sns.set_palette(\"husl\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 1. Data Setup and Configuration\n", + "\n", + "First, let's set up our data paths and examine the mooring configuration." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Set your data paths here\n", + "basedir = '/Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/'\n", + "mooring_name = 'dsE_1_2018'\n", + "\n", + "# Construct paths\n", + "proc_dir = Path(basedir) / 'moor' / 'proc' / mooring_name\n", + "config_file = proc_dir / f\"{mooring_name}.mooring.yaml\"\n", + "\n", + "print(f\"Processing directory: {proc_dir}\")\n", + "print(f\"Configuration file: {config_file}\")\n", + "print(f\"Config exists: {config_file.exists()}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load and examine the mooring configuration\n", + "if config_file.exists():\n", + " with open(config_file, 'r') as f:\n", + " config = yaml.safe_load(f)\n", + "\n", + " print(\"Mooring Configuration:\")\n", + " print(f\"Name: {config['name']}\")\n", + " print(f\"Water depth: {config.get('waterdepth', 'unknown')} m\")\n", + " print(f\"Location: {config.get('latitude', 'unknown')}°N, {config.get('longitude', 'unknown')}°E\")\n", + " print(f\"\\nInstruments ({len(config.get('instruments', []))}):\")\n", + "\n", + " for i, inst in enumerate(config.get('instruments', [])):\n", + " print(f\" {i+1}. {inst.get('instrument', 'unknown')} \"\n", + " f\"(serial: {inst.get('serial num.', 'unknown')}) at {inst.get('depth', 'unknown')} m\")\n", + "else:\n", + " print(\"Configuration file not found!\")\n", + " print(\"Please check your data path and mooring name.\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 2. Examine Individual Instrument Files\n", + "\n", + "Let's look at the individual instrument files before processing to understand the different sampling rates and data characteristics." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Find and examine individual instrument files\n", + "file_suffix = \"_use\"\n", + "instrument_files = []\n", + "instrument_datasets = []\n", + "rows = []\n", + "\n", + "if config_file.exists():\n", + " for inst_config in config.get(\"instruments\", []):\n", + " instrument_type = inst_config.get(\"instrument\", \"unknown\")\n", + " serial = inst_config.get(\"serial\", 0)\n", + " depth = inst_config.get(\"depth\", 0)\n", + "\n", + " # Look for the file\n", + " filename = f\"{mooring_name}_{serial}{file_suffix}.nc\"\n", + " filepath = proc_dir / instrument_type / filename\n", + "\n", + " if filepath.exists():\n", + " ds = xr.open_dataset(filepath)\n", + " instrument_files.append(filepath)\n", + " instrument_datasets.append(ds)\n", + "\n", + " # Time coverage\n", + " t0, t1 = ds.time.values[0], ds.time.values[-1]\n", + " npoints = len(ds.time)\n", + "\n", + " # Median sampling interval\n", + " time_diff = np.diff(ds.time.values) / np.timedelta64(1, \"m\") # in minutes\n", + " median_interval = np.nanmedian(time_diff)\n", + " if median_interval > 1:\n", + " sampling = f\"{median_interval:.1f} min\"\n", + " else:\n", + " sampling = f\"{median_interval*60:.1f} sec\"\n", + "\n", + " # Collect a row for the table\n", + " rows.append(\n", + " {\n", + " \"Instrument\": instrument_type,\n", + " \"Serial\": serial,\n", + " \"Depth [m]\": depth,\n", + " \"File\": filepath.name,\n", + " \"Start\": str(t0)[:19],\n", + " \"End\": str(t1)[:19],\n", + " \"Points\": npoints,\n", + " \"Sampling\": sampling,\n", + " \"Variables\": \", \".join(list(ds.data_vars)),\n", + " }\n", + " )\n", + " else:\n", + " rows.append(\n", + " {\n", + " \"Instrument\": instrument_type,\n", + " \"Serial\": serial,\n", + " \"Depth [m]\": depth,\n", + " \"File\": \"MISSING\",\n", + " \"Start\": \"\",\n", + " \"End\": \"\",\n", + " \"Points\": 0,\n", + " \"Sampling\": \"\",\n", + " \"Variables\": \"\",\n", + " }\n", + " )\n", + "\n", + " # Make a DataFrame summary\n", + " summary = pd.DataFrame(rows)\n", + " pd.set_option(\"display.max_colwidth\", 80) # allow long var lists\n", + " print(summary.to_markdown(index=False))\n", + "\n", + " print(f\"\\nFound {len(instrument_datasets)} instrument datasets\")\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 4. Process with Time Gridding (No Filtering)\n", + "\n", + "First, let's process the mooring without any filtering to see the basic time gridding functionality." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Process without filtering\n", + "print(\"Processing mooring with time gridding only (no filtering)...\")\n", + "print(\"=\"*60)\n", + "\n", + "result = time_gridding_mooring(mooring_name, basedir, file_suffix='_use')\n", + "\n", + "print(f\"\\nProcessing result: {'SUCCESS' if result else 'FAILED'}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load and examine the combined dataset\n", + "output_file = proc_dir / f\"{mooring_name}_mooring_use.nc\"\n", + "\n", + "if output_file.exists():\n", + " print(f\"Output file exists: {output_file}\")\n", + "\n", + " # Load the combined dataset\n", + " combined_ds = xr.open_dataset(output_file)\n", + "else:\n", + " print(\"Output file not found - processing may have failed\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 5. Visualize Combined Dataset\n", + "\n", + "Let's plot the combined dataset to see how the different instruments look on the common time grid." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "import matplotlib.pyplot as plt\n", + "\n", + "def plot_combined_timeseries(\n", + " combined_ds,\n", + " variables=(\"temperature\", \"salinity\", \"pressure\"),\n", + " cmap_name=\"viridis\",\n", + " line_alpha=0.8,\n", + " line_width=1.2,\n", + " percentile_limits=(1, 99),\n", + "):\n", + " \"\"\"\n", + " Plot selected variables from a combined mooring dataset as stacked time series.\n", + "\n", + " Parameters\n", + " ----------\n", + " combined_ds : xarray.Dataset\n", + " Must have dims: time, N_LEVELS. Optional coords: nominal_depth, serial_number.\n", + " variables : iterable[str]\n", + " Variable names to try to plot (if present in dataset).\n", + " cmap_name : str\n", + " Matplotlib colormap name for coloring by instrument level.\n", + " line_alpha : float\n", + " Line transparency.\n", + " line_width : float\n", + " Line width.\n", + " percentile_limits : (low, high)\n", + " Percentiles to use for automatic y-limits (e.g., (1, 99)).\n", + " \"\"\"\n", + " if combined_ds is None:\n", + " print(\"Combined dataset not available.\")\n", + " return None, None\n", + " n_levels = combined_ds.sizes.get(\"N_LEVELS\")\n", + " if n_levels is None:\n", + " raise ValueError(\"Dataset must contain dimension 'N_LEVELS'.\")\n", + "\n", + " available = [v for v in variables if v in combined_ds.data_vars]\n", + " if not available:\n", + " print(\"No requested variables found to plot.\")\n", + " return None, None\n", + "\n", + " # Colors by level\n", + " cmap = plt.get_cmap(cmap_name)\n", + " colors = cmap(np.linspace(0, 1, n_levels))\n", + "\n", + " fig, axes = plt.subplots(\n", + " len(available), 1, figsize=(14, 3.6 * len(available)), sharex=True, constrained_layout=True\n", + " )\n", + " if len(available) == 1:\n", + " axes = [axes]\n", + "\n", + " depth_arr = combined_ds.get(\"nominal_depth\")\n", + " serial_arr = combined_ds.get(\"serial_number\")\n", + "\n", + " first_axis = True\n", + " for ax, var in zip(axes, available):\n", + " values_for_limits = []\n", + " for level in range(n_levels):\n", + " depth = None if depth_arr is None else depth_arr.values[level]\n", + " serial = None if serial_arr is None else serial_arr.values[level]\n", + " label = None\n", + " if first_axis:\n", + " if depth is not None and np.isfinite(depth):\n", + " label = f\"Serial {serial} ({int(depth)} m)\" if serial is not None else f\"({int(depth)} m)\"\n", + " elif serial is not None:\n", + " label = f\"Serial {serial}\"\n", + "\n", + " da = combined_ds[var].isel(N_LEVELS=level)\n", + " da = da.where(np.isfinite(da), drop=True)\n", + " if da.size == 0:\n", + " continue\n", + "\n", + " values_for_limits.append(da.values)\n", + "\n", + " ax.plot(\n", + " da[\"time\"].values,\n", + " da.values,\n", + " color=colors[level],\n", + " alpha=line_alpha,\n", + " linewidth=line_width,\n", + " label=label,\n", + " )\n", + "\n", + " # Set labels and grid\n", + " ax.set_ylabel(var.replace(\"_\", \" \").title())\n", + " ax.grid(True, alpha=0.3)\n", + " ax.set_title(f\"{var.replace('_', ' ').title()} — Combined Time Grid\")\n", + "\n", + " # Legend only once\n", + " if first_axis:\n", + " ax.legend(ncol=3, fontsize=8, loc=\"upper right\", frameon=False)\n", + " first_axis = False\n", + "\n", + " # Auto y-limits based on percentiles\n", + " if values_for_limits:\n", + " flat = np.concatenate(values_for_limits)\n", + " low, high = np.nanpercentile(flat, percentile_limits)\n", + " ax.set_ylim(low, high)\n", + "\n", + " axes[-1].set_xlabel(\"Time\")\n", + " return fig, axes\n", + "\n", + "# Usage:\n", + "if 'combined_ds' in locals():\n", + " plot_combined_timeseries(combined_ds)\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 6. Process with Low-pass Filtering (RAPID-style)\n", + "\n", + "Now let's apply the RAPID-style 2-day low-pass filter to remove tidal and inertial variability. Remember: **filtering is applied to individual instruments BEFORE interpolation**." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Process with RAPID-style low-pass filtering\n", + "print(\"Processing mooring with 2-day low-pass filtering (RAPID-style)...\")\n", + "print(\"=\"*60)\n", + "print(\"IMPORTANT: Filtering is applied to each instrument on its native time grid\")\n", + "print(\"BEFORE interpolation to preserve data integrity.\")\n", + "print()\n", + "\n", + "filter_params = {\n", + " 'cutoff_days': 2.0, # 2-day cutoff\n", + " 'order': 6 # 6th order Butterworth\n", + "}\n", + "\n", + "result_filtered = time_gridding_mooring(\n", + " mooring_name, basedir,\n", + " file_suffix='_use',\n", + " filter_type='lowpass',\n", + " filter_params=filter_params\n", + ")\n", + "\n", + "print(f\"\\nFiltered processing result: {'SUCCESS' if result_filtered else 'FAILED'}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Load the filtered dataset\n", + "filtered_output_file = proc_dir / f\"{mooring_name}_mooring_use_lowpass.nc\"\n", + "\n", + "if filtered_output_file.exists():\n", + " print(f\"Filtered output file created: {filtered_output_file}\")\n", + "\n", + " # Load the filtered dataset\n", + " filtered_ds = xr.open_dataset(filtered_output_file)\n", + "\n", + " print(\"\\nFiltered Dataset Attributes:\")\n", + " filter_attrs = {k: v for k, v in filtered_ds.attrs.items()\n", + " if 'filter' in k.lower()}\n", + " for key, value in filter_attrs.items():\n", + " print(f\" {key}: {value}\")\n", + "\n", + " print(f\"\\nDataset shape: {dict(filtered_ds.dims)}\")\n", + "else:\n", + " print(\"Filtered output file not found\")\n", + " filtered_ds = None" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 7. Compare Filtered vs Unfiltered Data\n", + "\n", + "Let's compare the original and filtered data to see the effect of the low-pass filter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if 'combined_ds' in locals() and filtered_ds is not None:\n", + " # Compare filtered vs unfiltered for a subset of data\n", + " # Select a 10-day window for detailed comparison\n", + " start_time = combined_ds.time.values[len(combined_ds.time)//4] # Start 1/4 through\n", + " end_time = start_time + np.timedelta64(10, 'D') # 10-day window\n", + "\n", + " # Select subset\n", + " subset_orig = combined_ds.sel(time=slice(start_time, end_time))\n", + " subset_filt = filtered_ds.sel(time=slice(start_time, end_time))\n", + "\n", + " # Plot comparison for temperature\n", + " if 'temperature' in subset_orig.data_vars:\n", + " fig, axes = plt.subplots(2, 1, figsize=(14, 8), sharex=True)\n", + "\n", + " # Choose a representative level (first one with data)\n", + " level = 0\n", + " depth = subset_orig.nominal_depth.values[level]\n", + " serial = subset_orig.serial_number.values[level]\n", + "\n", + " # Original data\n", + " orig_temp = subset_orig.temperature.isel(N_LEVELS=level)\n", + " axes[0].plot(orig_temp.time, orig_temp, 'b-', alpha=0.7, linewidth=1,\n", + " label='Original')\n", + " axes[0].set_ylabel('Temperature (°C)')\n", + " axes[0].set_title(f'Original Data - Serial {serial} at {depth}m')\n", + " axes[0].grid(True, alpha=0.3)\n", + " axes[0].legend()\n", + "\n", + " # Filtered data\n", + " filt_temp = subset_filt.temperature.isel(N_LEVELS=level)\n", + " axes[1].plot(filt_temp.time, filt_temp, 'r-', alpha=0.7, linewidth=1.5,\n", + " label='2-day Low-pass Filtered')\n", + " axes[1].set_ylabel('Temperature (°C)')\n", + " axes[1].set_xlabel('Time')\n", + " axes[1].set_title(f'Filtered Data - Serial {serial} at {depth}m')\n", + " axes[1].grid(True, alpha=0.3)\n", + " axes[1].legend()\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + " # Overlay comparison\n", + " fig, ax = plt.subplots(1, 1, figsize=(14, 6))\n", + " ax.plot(orig_temp.time, orig_temp, 'b-', alpha=0.5, linewidth=0.8,\n", + " label='Original')\n", + " ax.plot(filt_temp.time, filt_temp, 'r-', alpha=0.8, linewidth=2,\n", + " label='2-day Low-pass Filtered')\n", + " ax.set_ylabel('Temperature (°C)')\n", + " ax.set_xlabel('Time')\n", + " ax.set_title(f'Filtering Comparison - Serial {serial} at {depth}m')\n", + " ax.legend()\n", + " ax.grid(True, alpha=0.3)\n", + " plt.tight_layout()\n", + " plt.show()\n", + "else:\n", + " print(\"Cannot compare - one or both datasets not available\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 8. Spectral Analysis: Effect of Filtering\n", + "\n", + "Let's examine the spectral characteristics to see how the filter affects different frequency components." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "if 'combined_ds' in locals() and filtered_ds is not None:\n", + " from scipy import signal\n", + "\n", + " # Select a level with good data coverage\n", + " level = 0\n", + "\n", + " # Get temperature data\n", + " if 'temperature' in combined_ds.data_vars:\n", + " orig_temp = combined_ds.temperature.isel(N_LEVELS=level).dropna('time')\n", + " filt_temp = filtered_ds.temperature.isel(N_LEVELS=level).dropna('time')\n", + "\n", + " if len(orig_temp) > 100: # Ensure sufficient data\n", + " # Calculate sampling rate\n", + " dt_hours = float(np.median(np.diff(orig_temp.time.values)) / np.timedelta64(1, 'h'))\n", + " fs = 1.0 / dt_hours # samples per hour\n", + "\n", + " # Compute power spectral density\n", + " f_orig, psd_orig = signal.welch(orig_temp.values, fs=fs, nperseg=min(256, len(orig_temp)//4))\n", + " f_filt, psd_filt = signal.welch(filt_temp.values, fs=fs, nperseg=min(256, len(filt_temp)//4))\n", + "\n", + " # Convert frequency to period in days\n", + " period_orig = 1.0 / (f_orig * 24) # days\n", + " period_filt = 1.0 / (f_filt * 24) # days\n", + "\n", + " # Plot power spectral density\n", + " fig, ax = plt.subplots(1, 1, figsize=(12, 6))\n", + "\n", + " ax.loglog(period_orig[1:], psd_orig[1:], 'b-', alpha=0.7,\n", + " label='Original', linewidth=1.5)\n", + " ax.loglog(period_filt[1:], psd_filt[1:], 'r-', alpha=0.8,\n", + " label='2-day Low-pass Filtered', linewidth=2)\n", + "\n", + " # Mark important periods\n", + " ax.axvline(2.0, color='gray', linestyle='--', alpha=0.7,\n", + " label='2-day cutoff')\n", + " ax.axvline(1.0, color='gray', linestyle=':', alpha=0.7,\n", + " label='1-day (diurnal)')\n", + " ax.axvline(0.5, color='gray', linestyle=':', alpha=0.7,\n", + " label='12-hour (semidiurnal)')\n", + "\n", + " ax.set_xlabel('Period (days)')\n", + " ax.set_ylabel('Power Spectral Density')\n", + " ax.set_title('Spectral Analysis: Effect of 2-day Low-pass Filter')\n", + " ax.legend()\n", + " ax.grid(True, alpha=0.3)\n", + " ax.set_xlim(0.1, 100)\n", + "\n", + " plt.tight_layout()\n", + " plt.show()\n", + "\n", + " print(f\"Spectral analysis completed for level {level}\")\n", + " print(f\"Sampling rate: {dt_hours:.2f} hours\")\n", + " print(f\"Data length: {len(orig_temp)} points\")\n", + " else:\n", + " print(\"Insufficient data for spectral analysis\")\n", + " else:\n", + " print(\"Temperature data not available for spectral analysis\")\n", + "else:\n", + " print(\"Cannot perform spectral analysis - datasets not available\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## 10. Multiple Mooring Processing Example\n", + "\n", + "The time gridding module also supports batch processing of multiple moorings." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "# Base directory containing the mooring data\n", + "basedir = '/Users/eddifying/Dropbox/data/ifmro_mixsed/ds_data_eleanor/'\n", + "\n", + "# Example of processing multiple moorings\n", + "# (This will only work if you have multiple moorings in your dataset)\n", + "\n", + "# List available moorings\n", + "moor_base = Path(basedir) / 'moor' / 'proc'\n", + "available_moorings = [d.name for d in moor_base.iterdir() if d.is_dir()]\n", + "\n", + "print(f\"Available moorings in {moor_base}:\")\n", + "for mooring in available_moorings[:5]: # Show first 5\n", + " print(f\" - {mooring}\")\n", + "\n", + "if len(available_moorings) > 5:\n", + " print(f\" ... and {len(available_moorings)-5} more\")\n", + "\n", + "# Example batch processing (commented out to avoid running on all moorings)\n", + "moorings_to_process = ['dsE_1_2018'] # Add your mooring names\n", + "\n", + "results = process_multiple_moorings_time_gridding(\n", + " moorings_to_process,\n", + " basedir,\n", + ")\n", + "\n", + "print(\"Batch processing results:\")\n", + "for mooring, success in results.items():\n", + " status = \"SUCCESS\" if success else \"FAILED\"\n", + " print(f\" {mooring}: {status}\")" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "venv (3.11.7)", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.7" + } + }, + "nbformat": 4, + "nbformat_minor": 4 } diff --git a/oceanarray/clock_offset.py b/oceanarray/clock_offset.py new file mode 100644 index 0000000..374ccf1 --- /dev/null +++ b/oceanarray/clock_offset.py @@ -0,0 +1,677 @@ +""" +Clock offset analysis functions for oceanographic instrument data. + +This module provides functions to analyze and detect clock offsets between +different instruments on the same mooring by comparing deployment timing +and performing lag correlation analysis. +""" + +import os + +import numpy as np +import pandas as pd +import xarray as xr +import yaml + +from oceanarray import find_deployment, tools + + +def load_mooring_instruments(mooring_name, base_dir, output_path, file_suffix="_raw"): + """ + Load all instruments for a mooring from netCDF files and enrich with YAML metadata. + + Parameters + ---------- + mooring_name : str + Name of the mooring (e.g., 'dsE_1_2018') + base_dir : str + Base directory path + output_path : str + Path to processed data directory + file_suffix : str, optional + File suffix to use ('_raw' or '_use'), default '_raw' + + Returns + ------- + list of xarray.Dataset + List of datasets for each instrument + dict + YAML metadata for the mooring + """ + proc_dir = output_path + mooring_name + moor_yaml = proc_dir + "/" + mooring_name + ".mooring.yaml" + + with open(moor_yaml, "r") as f: + moor_yaml_data = yaml.safe_load(f) + + datasets = [] + for i in moor_yaml_data["instruments"]: + fname = mooring_name + "_" + str(i["serial"]) + file_suffix + ".nc" + rawfile = proc_dir + "/" + i["instrument"] + "/" + fname + + if os.path.exists(rawfile): + print(rawfile) + ds1 = xr.open_dataset(rawfile) + + # Add metadata from YAML + if "InstrDepth" not in ds1.variables and "depth" in i: + ds1["InstrDepth"] = i["depth"] + if "instrument" not in ds1.variables and "instrument" in i: + ds1["instrument"] = i["instrument"] + if "serial_number" not in ds1.variables and "serial" in i: + ds1["serial_number"] = i["serial"] + if "timeS" in ds1.variables: + ds1 = ds1.drop_vars("timeS") + + datasets.append(ds1) + + return datasets, moor_yaml_data + + +def create_common_time_grid(datasets): + """ + Create a common time grid for interpolation based on all datasets. + + Parameters + ---------- + datasets : list of xarray.Dataset + List of instrument datasets + + Returns + ------- + numpy.ndarray + Common time grid as datetime64 array + """ + intervals_min = [] + start_times = [] + end_times = [] + + for ds in datasets: + time = ds["time"] + time_interval = np.nanmedian(np.diff(time.values) / np.timedelta64(1, "m")) + intervals_min.append(time_interval) + start_times.append(time.values[0]) + end_times.append(time.values[-1]) + + earliest_start = pd.to_datetime(start_times).min().to_datetime64() + + end_arr = np.array(end_times, dtype="datetime64[ns]") + mask = ~np.isnat(end_arr) + if mask.any(): + med_ns = np.median(end_arr[mask].astype("int64")) + latest_end = np.datetime64(int(med_ns), "ns") + else: + latest_end = np.datetime64("NaT", "ns") + + dt_sec = int(np.nanmedian(intervals_min) * 60) + time_grid = np.arange(earliest_start, latest_end, np.timedelta64(dt_sec, "s")) + + return time_grid + + +def interpolate_datasets_to_grid(datasets, time_grid): + """ + Interpolate all datasets to a common time grid. + + Parameters + ---------- + datasets : list of xarray.Dataset + List of instrument datasets + time_grid : numpy.ndarray + Common time grid for interpolation + + Returns + ------- + list of xarray.Dataset + List of interpolated datasets + """ + datasets_interp = [] + + for idx, ds in enumerate(datasets): + if "time" not in ds.sizes: + print( + f" WARNING: Dataset {idx} has no time dimension, skipping interpolation" + ) + continue + + if ds.sizes["time"] <= 1: + print( + f" WARNING: Dataset {idx} has only {ds.sizes['time']} time point(s), skipping interpolation" + ) + continue + + try: + interp_vars = {} + for var in ds.data_vars: + if "time" in ds[var].dims: + interp_vars[var] = ds[var].interp(time=time_grid) + else: + interp_vars[var] = ds[var] + + if interp_vars: + ds_interp = xr.Dataset(interp_vars, coords={"time": time_grid}) + + if "InstrDepth" in ds: + ds_interp = ds_interp.assign_coords(depth=ds["InstrDepth"]) + if "clock_offset" in ds: + ds_interp = ds_interp.assign_coords( + seconds_offset=ds["clock_offset"] + ) + + datasets_interp.append(ds_interp) + else: + print(f" No time-dependent variables found in dataset {idx}") + + except Exception as e: + print(f" ERROR interpolating dataset {idx}: {e}") + continue + + return datasets_interp + + +def combine_interpolated_datasets(datasets_interp): + """ + Combine interpolated datasets into a single multi-level dataset. + + Parameters + ---------- + datasets_interp : list of xarray.Dataset + List of interpolated datasets + + Returns + ------- + xarray.Dataset + Combined dataset with N_LEVELS dimension + """ + vars_to_keep = [ + "temperature", + "salinity", + "conductivity", + "pressure", + "u_velocity", + "v_velocity", + ] + + datasets_clean = [] + for ds in datasets_interp: + ds_sel = ds.drop_vars( + ["density", "potential_temperature", "julian_days_offset", "timeS"], + errors="ignore", + ) + datasets_clean.append(ds_sel) + + time_coord = datasets_interp[0]["time"] + combined_data = {} + N_LEVELS = len(datasets_clean) + + for var in vars_to_keep: + arrs = [] + for ds in datasets_clean: + if var in ds: + arrs.append(ds[var].values) + else: + arrs.append(np.full(time_coord.shape, np.nan)) + combined_data[var] = (("time", "N_LEVELS"), np.stack(arrs, axis=-1)) + + # Gather metadata for each level + depths = [] + clock_offsets = [] + serial = [] + instrtype = [] + + for ds in datasets_clean: + depths.append(float(ds["InstrDepth"].item()) if "InstrDepth" in ds else np.nan) + serial.append(ds["serial_number"].item() if "serial_number" in ds else np.nan) + instrtype.append(ds["instrument"].item() if "instrument" in ds else "unknown") + + if "clock_offset" in ds: + co = ds["clock_offset"].item() + elif "seconds_offset" in ds: + co = ds["seconds_offset"].item() + else: + co = 0 + clock_offsets.append(int(np.rint(co)) if np.isfinite(co) else 0) + + combined_ds = xr.Dataset( + data_vars=combined_data, + coords={ + "time": time_coord, + "N_LEVELS": np.arange(N_LEVELS), + "clock_offset": ("N_LEVELS", np.array(clock_offsets)), + "serial_number": ("N_LEVELS", np.array(serial)), + "nominal_depth": ("N_LEVELS", np.array(depths)), + "instrument": ("N_LEVELS", np.asarray(instrtype)), + }, + ) + + return combined_ds + + +def analyze_deployment_timing(combined_ds): + """ + Analyze deployment timing using temperature-based detection of deployment periods. + + Parameters + ---------- + combined_ds : xarray.Dataset + Combined dataset with all instruments + + Returns + ------- + xarray.Dataset + Dataset enriched with deployment timing information + """ + return find_deployment.find_deployment( + combined_ds, bottom_strategy="deployment_bounds" + ) + + +def calculate_timing_offsets(combined_ds, bin_width_sec=60): + """ + Calculate timing offsets between instruments based on deployment start/end times. + + Parameters + ---------- + combined_ds : xarray.Dataset + Dataset with deployment timing information + bin_width_sec : float, optional + Bin width in seconds for consensus grouping, default 60 + + Returns + ------- + dict + Dictionary containing offset analysis results + """ + start_times = pd.to_datetime(combined_ds["start_time"].values) + end_times = pd.to_datetime(combined_ds["end_time"].values) + + f_start = np.isfinite(start_times) + f_end = np.isfinite(end_times) + + # Initial reference times for clustering + ref_start0 = start_times[f_start].min() + ref_end0 = end_times[f_end].max() + + start_off0 = np.full(start_times.shape, np.nan, float) + end_off0 = np.full(end_times.shape, np.nan, float) + start_off0[f_start] = (start_times[f_start] - ref_start0) / np.timedelta64(1, "s") + end_off0[f_end] = (end_times[f_end] - ref_end0) / np.timedelta64(1, "s") + + # Find consensus group + vals = start_off0[np.isfinite(start_off0)] + if vals.size == 0: + raise RuntimeError("No finite start offsets to form consensus.") + + vmin, vmax = vals.min(), vals.max() + bins = np.arange(vmin - bin_width_sec, vmax + 2 * bin_width_sec, bin_width_sec) + hist, edges = np.histogram(vals, bins=bins) + k = np.argmax(hist) + lo, hi = edges[k], edges[k + 1] + in_consensus = (start_off0 >= lo) & (start_off0 < hi) + + idx_consensus = np.where(in_consensus & f_start & f_end)[0] + if idx_consensus.size == 0: + idx_consensus = np.where(in_consensus & f_start)[0] + + # Redefine references from consensus group + ref_start = start_times[idx_consensus].min() + ref_end = end_times[idx_consensus].max() + + # Recompute offsets + start_off = (start_times - ref_start) / np.timedelta64(1, "s") + end_off = (end_times - ref_end) / np.timedelta64(1, "s") + avg_off = (start_off + end_off) / 2.0 + diff_off = start_off - end_off + + # Calculate drift rates + dur = (end_times - start_times) / np.timedelta64(1, "s") + drift_rate_per_day = np.full_like(avg_off, np.nan, dtype=float) + ok = np.isfinite(start_off) & np.isfinite(end_off) & np.isfinite(dur) & (dur > 0) + drift_rate_per_day[ok] = (end_off[ok] - start_off[ok]) / dur[ok] * 86400.0 + + return { + "start_offsets": start_off, + "end_offsets": end_off, + "avg_offsets": avg_off, + "diff_offsets": diff_off, + "drift_rates": drift_rate_per_day, + "consensus_indices": idx_consensus, + "ref_start": ref_start, + "ref_end": ref_end, + } + + +def perform_lag_correlation_analysis(combined_ds, ref_index=0, sub_sample=5): + """ + Perform lag correlation analysis between instruments to detect clock offsets. + + Parameters + ---------- + combined_ds : xarray.Dataset + Combined dataset with all instruments + ref_index : int, optional + Index of reference instrument, default 0 + sub_sample : int, optional + Subsampling factor to speed up correlation, default 5 + + Returns + ------- + dict + Dictionary containing correlation analysis results + """ + time_interval = np.nanmedian( + np.diff(combined_ds["time"].values) / np.timedelta64(1, "s") + ) + + n_full = len(combined_ds["temperature"][:, ref_index].values) + ref_temp_sub = combined_ds["temperature"][:, ref_index].values[::sub_sample] + n_sub = len(ref_temp_sub) + + max_lag_sub = n_sub // 5 + lags_sub = np.arange(-max_lag_sub, max_lag_sub + 1) + + results = { + "ref_index": ref_index, + "sub_sample": sub_sample, + "time_interval": time_interval, + "lags": [], + "correlations": [], + "max_correlations": [], + "clock_offsets": [], + } + + N_LEVELS = combined_ds.sizes["N_LEVELS"] + + for i in range(N_LEVELS): + temp_i_sub = combined_ds["temperature"][:, i].values[::sub_sample] + coff = combined_ds["clock_offset"][i].values + + corrs_sub = tools.lag_correlation(ref_temp_sub, temp_i_sub, max_lag_sub) + + max_corr_idx = np.nanargmax(corrs_sub) + max_corr = corrs_sub[max_corr_idx] + max_lag = lags_sub[max_corr_idx] + dt_sub = sub_sample * time_interval + clock_offset_total = max_lag * dt_sub + coff + + results["lags"].append(max_lag) + results["correlations"].append(corrs_sub) + results["max_correlations"].append(max_corr) + results["clock_offsets"].append(clock_offset_total) + + return results + + +def print_timing_offset_summary(combined_ds, offset_results): + """ + Print a summary table of timing offsets for all instruments. + + Parameters + ---------- + combined_ds : xarray.Dataset + Combined dataset with instrument metadata + offset_results : dict + Results from calculate_timing_offsets() + """ + N = combined_ds.sizes["N_LEVELS"] + labels = combined_ds["instrument"].values + serial = combined_ds["serial_number"].values + + start_times = pd.to_datetime(combined_ds["start_time"].values) + end_times = pd.to_datetime(combined_ds["end_time"].values) + + print(f"Consensus group size: {offset_results['consensus_indices'].size}") + print( + f"Consensus-derived refs -> ref_start={offset_results['ref_start']}, ref_end={offset_results['ref_end']}\n" + ) + + for i in range(N): + tag = "REF" if i in offset_results["consensus_indices"] else "-" + s = offset_results["start_offsets"][i] + e = offset_results["end_offsets"][i] + a = offset_results["avg_offsets"][i] + d = offset_results["diff_offsets"][i] + dr = offset_results["drift_rates"][i] + + print( + f"{i:02d}: {str(labels[i]):8s}/{str(serial[i]):6s} | " + f"start={pd.to_datetime(start_times[i])} ({s:+8.0f}s) | " + f"end={pd.to_datetime(end_times[i])} ({e:+8.0f}s) | " + f"avg={a:+8.0f}s | diff={d:+6.0f}s | drift={'nan' if not np.isfinite(dr) else f'{dr:+.2f} s/day'} | {tag}" + ) + + +def suggest_reference_instrument(combined_ds, offset_results): + """ + Suggest the best reference instrument for lag correlation analysis. + + This function recommends the instrument with the smallest average timing offset + from the temperature threshold method, making it the most likely to have + accurate timing for use as a correlation reference. + + Parameters + ---------- + combined_ds : xarray.Dataset + Combined dataset with instrument metadata + offset_results : dict + Results from calculate_timing_offsets() + + Returns + ------- + dict + Dictionary with recommendation details including suggested index + """ + avg_offsets = offset_results["avg_offsets"] + abs_offsets = np.abs(avg_offsets) + + # Find instruments with finite offsets + finite_mask = np.isfinite(abs_offsets) + if not finite_mask.any(): + print("Warning: No instruments have finite timing offsets") + return {"suggested_index": 0, "reason": "fallback - no finite offsets"} + + # Find the instrument with smallest absolute average offset + finite_indices = np.where(finite_mask)[0] + min_abs_idx = finite_indices[np.argmin(abs_offsets[finite_mask])] + + instruments = combined_ds["instrument"].values + serial = combined_ds["serial_number"].values + depths = combined_ds["nominal_depth"].values + + suggestion = { + "suggested_index": min_abs_idx, + "offset_seconds": avg_offsets[min_abs_idx], + "abs_offset_seconds": abs_offsets[min_abs_idx], + "instrument": instruments[min_abs_idx], + "serial": serial[min_abs_idx], + "depth": depths[min_abs_idx], + "reason": "smallest absolute average timing offset", + } + + print("Suggested reference instrument for lag correlation:") + print( + f" Index {min_abs_idx}: {instruments[min_abs_idx]} #{serial[min_abs_idx]} at {depths[min_abs_idx]:.0f}m" + ) + print(f" Average timing offset: {avg_offsets[min_abs_idx]:+.1f}s") + print(f" Reason: {suggestion['reason']}") + print() + + return suggestion + + +def print_correlation_summary(combined_ds, correlation_results): + """ + Print a summary of lag correlation analysis results. + + Parameters + ---------- + combined_ds : xarray.Dataset + Combined dataset with instrument metadata + correlation_results : dict + Results from perform_lag_correlation_analysis() + """ + serial = combined_ds["serial_number"].values + depths = combined_ds["nominal_depth"].values + + print("Lag Correlation Analysis Results:") + print("(Enter the summed value, no sign change, in the yaml as clock_offset)") + print() + + for i, (lag, corr, offset) in enumerate( + zip( + correlation_results["lags"], + correlation_results["max_correlations"], + correlation_results["clock_offsets"], + ) + ): + print( + f"Level {i+1} (#{serial[i]}): max corr = {corr:.3f} @lag {lag} --> clock_offset: {offset:.0f}s" + ) + + +def plot_deployment_boundaries( + original_datasets, combined_ds, n_samples=10, figsize=(12, 4) +): + """ + Plot detailed view of deployment boundaries showing individual measurements. + + This function creates plots showing the exact transition points where instruments + move from surface to deployment conditions, with individual data points highlighted + around the predicted start and end times. + + Parameters + ---------- + original_datasets : list of xarray.Dataset + Original (non-interpolated) datasets for each instrument + combined_ds : xarray.Dataset + Combined dataset with deployment timing information + n_samples : int, optional + Number of samples to show before/after predicted boundaries, default 10 + figsize : tuple, optional + Figure size for each plot, default (12, 4) + """ + import matplotlib.pyplot as plt + + start_times = combined_ds["start_time"].values + end_times = combined_ds["end_time"].values + split_vals = combined_ds["split_value"].values + instruments = combined_ds["instrument"].values + depths = combined_ds["nominal_depth"].values + + for i, ds in enumerate(original_datasets): + if i >= len(start_times): + break + + if "temperature" not in ds.data_vars: + print(f"Skipping instrument {i}: no temperature data") + continue + + time_orig = ds["time"].values + temp_orig = ds["temperature"].values + + # Convert deployment times to numpy datetime64 for comparison + start_time = start_times[i] + end_time = end_times[i] + split_val = split_vals[i] + + # Find indices closest to start and end times + start_idx = None + end_idx = None + + if np.isfinite(start_time.astype("datetime64[ns]").astype("int64")): + start_idx = np.argmin(np.abs(time_orig - start_time)) + + if np.isfinite(end_time.astype("datetime64[ns]").astype("int64")): + end_idx = np.argmin(np.abs(time_orig - end_time)) + + # Create subplots for start and end (if both exist) + if start_idx is not None and end_idx is not None: + fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(figsize[0] * 2, figsize[1])) + axes = [ax1, ax2] + titles = ["Deployment Start", "Deployment End"] + indices = [start_idx, end_idx] + times_ref = [start_time, end_time] + elif start_idx is not None: + fig, ax1 = plt.subplots(1, 1, figsize=figsize) + axes = [ax1] + titles = ["Deployment Start"] + indices = [start_idx] + times_ref = [start_time] + elif end_idx is not None: + fig, ax1 = plt.subplots(1, 1, figsize=figsize) + axes = [ax1] + titles = ["Deployment End"] + indices = [end_idx] + times_ref = [end_time] + else: + print(f"Skipping instrument {i}: no valid deployment times") + continue + + # Plot each boundary + for ax, title, idx, time_ref in zip(axes, titles, indices, times_ref): + # Define range around the boundary + start_range = max(0, idx - n_samples) + end_range = min(len(time_orig), idx + n_samples + 1) + + time_window = time_orig[start_range:end_range] + temp_window = temp_orig[start_range:end_range] + + # Plot connecting line first (so points appear on top) + ax.plot( + time_window, + temp_window, + "b-", + linewidth=1.5, + alpha=0.8, + label="Temperature", + ) + + # Plot individual measurements as filled red circles + ax.plot( + time_window, + temp_window, + "ro", + markersize=6, + markerfacecolor="red", + markeredgecolor="darkred", + markeredgewidth=1, + label="Measurements", + ) + + # Highlight the exact predicted boundary time + ax.axvline( + time_ref, + color="green", + linestyle="--", + linewidth=2, + alpha=0.8, + label=f"Predicted {title.split()[1]}", + ) + + # Add horizontal line for split value + ax.axhline( + split_val, + color="orange", + linestyle=":", + linewidth=1.5, + alpha=0.7, + label=f"Split Value ({split_val:.2f}°C)", + ) + + ax.set_xlabel("Time") + ax.set_ylabel("Temperature (°C)") + ax.set_title(f"{title} - {instruments[i]} at {depths[i]:.0f}m") + ax.legend(loc="best") + ax.grid(True, alpha=0.3) + + # Format x-axis for better readability + import matplotlib.dates as mdates + + ax.xaxis.set_major_formatter(mdates.DateFormatter("%H:%M:%S")) + ax.xaxis.set_major_locator( + mdates.MinuteLocator(interval=max(1, n_samples // 5)) + ) + plt.setp(ax.xaxis.get_majorticklabels(), rotation=45) + + plt.tight_layout() + plt.show() diff --git a/oceanarray/config/logging.yaml b/oceanarray/config/logging.yaml new file mode 100644 index 0000000..1c18413 --- /dev/null +++ b/oceanarray/config/logging.yaml @@ -0,0 +1,28 @@ +# Global logging configuration for oceanarray processing stages +# +# This configuration file controls where and how log files are created +# for all processing stages (stage1, stage2, time_gridding, etc.) + +logging: + # Directory for log files + # - Can be relative path (created within each mooring's proc directory) + # - Can be absolute path for centralized logging + # - Default: "processing_logs" creates ~/moor/proc//processing_logs/ + directory: "processing_logs" + + # Log filename pattern + # Available variables: + # {mooring_name} - Name of the mooring (e.g., dsE_1_2018) + # {timestamp} - Formatted timestamp + # {stage} - Processing stage name (stage1, stage2, time_gridding) + filename_pattern: "{mooring_name}_{timestamp}_{stage}.log" + + # Timestamp format for log filenames + # Uses Python strftime format codes: + # %Y = 4-digit year %m = month %d = day + # %H = hour (24hr) %M = minute %S = second + # Default: "%Y%m%dT%H" produces YYYYMMDTHH (e.g., 20180827T14) + timestamp_format: "%Y%m%dT%H" + + # Whether to create the log directory if it doesn't exist + create_directory: true diff --git a/oceanarray/logger.py b/oceanarray/logger.py index 4409967..5f59cf1 100644 --- a/oceanarray/logger.py +++ b/oceanarray/logger.py @@ -93,3 +93,98 @@ def setup_logger(array_name: str, output_dir: str = "logs") -> None: log.addHandler(console_handler) log.info(f"Logger initialized for array: {array_name}, writing to {log_path}") + + +def load_logging_config(): + """ + Load the global logging configuration from config/logging.yaml. + + Returns + ------- + dict + Logging configuration dictionary + + Raises + ------ + FileNotFoundError + If logging.yaml is not found + yaml.YAMLError + If logging.yaml cannot be parsed + """ + import yaml + + config_path = Path(__file__).parent / "config" / "logging.yaml" + + if not config_path.exists(): + raise FileNotFoundError(f"Logging config not found: {config_path}") + + with open(config_path, "r") as f: + config = yaml.safe_load(f) + + return config.get("logging", {}) + + +def setup_stage_logging(mooring_name: str, stage_name: str, proc_dir: Path) -> Path: + """ + Set up logging for a processing stage using global configuration. + + This creates simple file-based logging for processing stages (stage1, stage2, etc.) + using the configuration from config/logging.yaml. + + Parameters + ---------- + mooring_name : str + Name of the mooring (e.g., 'dsE_1_2018') + stage_name : str + Name of the processing stage (e.g., 'stage1', 'stage2', 'time_gridding') + proc_dir : Path + Processing directory for the mooring + + Returns + ------- + Path + Full path to the log file + + Raises + ------ + FileNotFoundError + If logging config cannot be loaded + """ + import yaml + + try: + config = load_logging_config() + except (FileNotFoundError, yaml.YAMLError): + # Fallback to old behavior if config is not available + log_time = datetime.datetime.now().strftime("%Y%m%dT%H") + return proc_dir / f"{mooring_name}_{log_time}_{stage_name}.mooring.log" + + # Extract configuration values with defaults + log_directory = config.get("directory", "logs") + filename_pattern = config.get( + "filename_pattern", "{mooring_name}_{timestamp}_{stage}.log" + ) + timestamp_format = config.get("timestamp_format", "%Y%m%dT%H") + create_directory = config.get("create_directory", True) + + # Generate timestamp + timestamp = datetime.datetime.now().strftime(timestamp_format) + + # Determine log directory path + if Path(log_directory).is_absolute(): + # Absolute path specified + log_dir = Path(log_directory) + else: + # Relative path - create within mooring proc directory + log_dir = proc_dir / log_directory + + # Create directory if requested and it doesn't exist + if create_directory: + log_dir.mkdir(parents=True, exist_ok=True) + + # Generate filename using pattern + filename = filename_pattern.format( + mooring_name=mooring_name, timestamp=timestamp, stage=stage_name + ) + + return log_dir / filename diff --git a/oceanarray/stage1.py b/oceanarray/stage1.py index 5802d21..67536e0 100644 --- a/oceanarray/stage1.py +++ b/oceanarray/stage1.py @@ -2,7 +2,6 @@ Refactored stage1 processing for mooring data with improved readability. """ -from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -44,9 +43,10 @@ def __init__(self, base_dir: str): self.log_file = None def _setup_logging(self, mooring_name: str, output_path: Path) -> None: - """Set up logging for the processing run.""" - log_time = datetime.now().strftime("%Y%m%dT%H") - self.log_file = output_path / f"{mooring_name}_{log_time}_stage1.mooring.log" + """Set up logging for the processing run using global config.""" + from .logger import setup_stage_logging + + self.log_file = setup_stage_logging(mooring_name, "stage1", output_path) def _log_print(self, *args, **kwargs) -> None: """Print to both console and log file.""" diff --git a/oceanarray/stage2.py b/oceanarray/stage2.py index 3a6d49b..3757b1d 100644 --- a/oceanarray/stage2.py +++ b/oceanarray/stage2.py @@ -8,7 +8,6 @@ - Writing updated NetCDF files with '_use' suffix """ -from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional @@ -28,9 +27,10 @@ def __init__(self, base_dir: str): self.log_file = None def _setup_logging(self, mooring_name: str, output_path: Path) -> None: - """Set up logging for the processing run.""" - log_time = datetime.now().strftime("%Y%m%dT%H") - self.log_file = output_path / f"{mooring_name}_{log_time}_stage2.mooring.log" + """Set up logging for the processing run using global config.""" + from .logger import setup_stage_logging + + self.log_file = setup_stage_logging(mooring_name, "stage2", output_path) def _log_print(self, *args, **kwargs) -> None: """Print to both console and log file.""" diff --git a/oceanarray/time_gridding.py b/oceanarray/time_gridding.py index 5d2b373..80226d1 100644 --- a/oceanarray/time_gridding.py +++ b/oceanarray/time_gridding.py @@ -21,7 +21,6 @@ Last updated: 2025-09-07 """ -from datetime import datetime from pathlib import Path from typing import Any, Dict, List, Optional, Tuple @@ -40,11 +39,10 @@ def __init__(self, base_dir: str): self.log_file = None def _setup_logging(self, mooring_name: str, output_path: Path) -> None: - """Set up logging for the processing run.""" - log_time = datetime.now().strftime("%Y%m%dT%H") - self.log_file = ( - output_path / f"{mooring_name}_{log_time}_time_gridding.mooring.log" - ) + """Set up logging for the processing run using global config.""" + from .logger import setup_stage_logging + + self.log_file = setup_stage_logging(mooring_name, "time_gridding", output_path) def _log_print(self, *args, **kwargs) -> None: """Print to both console and log file.""" @@ -453,9 +451,7 @@ def _analyze_timing_info( # Check for large differences in sampling rates interval_ratio = np.max(intervals_min) / np.min(intervals_min) if interval_ratio > 2.0: - self._log_print( - " WARNING: Large differences in sampling rates detected!" - ) + self._log_print(" WARNING: Large differences in sampling rates detected!") self._log_print( f" WARNING: Ratio of slowest to fastest: {interval_ratio:.1f}x" ) diff --git a/oceanarray/utilities.py b/oceanarray/utilities.py index 9413853..0fc698b 100644 --- a/oceanarray/utilities.py +++ b/oceanarray/utilities.py @@ -113,7 +113,7 @@ def get_time_key(ds): # Fallback: look for any variable or coordinate with datetime dtype for name in ds.coords: if np.issubdtype(ds.coords[name].dtype, np.datetime64): - log_warning( + logger.log_warning( "No standard time coordinate found. Using first datetime coordinate: %s", name, ) @@ -121,7 +121,7 @@ def get_time_key(ds): for name in ds.variables: if np.issubdtype(ds.variables[name].dtype, np.datetime64): - log_warning( + logger.log_warning( "No standard time coordinate found. Using first datetime coordinate: %s", name, ) diff --git a/tests/test_stage1.py b/tests/test_stage1.py index 4c07ff5..19a230c 100644 --- a/tests/test_stage1.py +++ b/tests/test_stage1.py @@ -85,9 +85,9 @@ def test_setup_logging(self, processor, temp_dir): processor._setup_logging(mooring_name, output_path) assert processor.log_file is not None - assert processor.log_file.parent == output_path + assert processor.log_file.parent == output_path / "processing_logs" assert mooring_name in processor.log_file.name - assert "stage1.mooring.log" in processor.log_file.name + assert "stage1.log" in processor.log_file.name def test_log_print(self, processor, temp_dir): """Test log printing functionality.""" @@ -259,7 +259,7 @@ def test_process_missing_file(self, test_data_setup): assert result is False # Check log file contains error message - log_files = list(setup["proc_dir"].glob("*_stage1.mooring.log")) + log_files = list(setup["proc_dir"].glob("processing_logs/*_stage1.log")) assert len(log_files) == 1 log_content = log_files[0].read_text() assert "Error reading file" in log_content @@ -278,7 +278,7 @@ def test_process_existing_output(self, test_data_setup): assert result2 is True # Check log mentions skipping - log_files = list(setup["proc_dir"].glob("*_stage1.mooring.log")) + log_files = list(setup["proc_dir"].glob("processing_logs/*_stage1.log")) log_content = log_files[-1].read_text() # Get the latest log assert "OUTFILE EXISTS" in log_content diff --git a/tests/test_stage2.py b/tests/test_stage2.py index 4044de1..20ee482 100644 --- a/tests/test_stage2.py +++ b/tests/test_stage2.py @@ -100,9 +100,9 @@ def test_setup_logging(self, processor, temp_dir): processor._setup_logging(mooring_name, output_path) assert processor.log_file is not None - assert processor.log_file.parent == output_path + assert processor.log_file.parent == output_path / "processing_logs" assert mooring_name in processor.log_file.name - assert "stage2.mooring.log" in processor.log_file.name + assert "stage2.log" in processor.log_file.name def test_read_yaml_time_valid(self, processor): """Test reading valid time from YAML.""" @@ -425,7 +425,7 @@ def test_process_missing_raw_file(self, test_data_setup): assert result is False # Check log file contains warning - log_files = list(setup["proc_dir"].glob("*_stage2.mooring.log")) + log_files = list(setup["proc_dir"].glob("processing_logs/*_stage2.log")) assert len(log_files) == 1 log_content = log_files[0].read_text() assert "Raw file not found" in log_content diff --git a/tests/test_time_gridding.py b/tests/test_time_gridding.py index 2f23240..67c8172 100644 --- a/tests/test_time_gridding.py +++ b/tests/test_time_gridding.py @@ -140,9 +140,9 @@ def test_setup_logging(self, processor, temp_dir): processor._setup_logging(mooring_name, output_path) assert processor.log_file is not None - assert processor.log_file.parent == output_path + assert processor.log_file.parent == output_path / "processing_logs" assert mooring_name in processor.log_file.name - assert "time_gridding.mooring.log" in processor.log_file.name + assert "time_gridding.log" in processor.log_file.name def test_ensure_instrument_metadata(self, processor): """Test metadata addition to datasets.""" @@ -479,7 +479,7 @@ def test_missing_instruments_warning(self, test_data_setup): assert result is True # Check log file mentions missing instrument - log_files = list(setup["proc_dir"].glob("*_time_gridding.mooring.log")) + log_files = list(setup["proc_dir"].glob("processing_logs/*_time_gridding.log")) assert len(log_files) == 1 log_content = log_files[0].read_text() assert "Missing instruments" in log_content @@ -495,7 +495,7 @@ def test_different_sampling_rates_warning(self, test_data_setup): assert result is True # Check log mentions timing analysis - log_files = list(setup["proc_dir"].glob("*_time_gridding.mooring.log")) + log_files = list(setup["proc_dir"].glob("processing_logs/*_time_gridding.log")) log_content = log_files[0].read_text() assert "TIMING ANALYSIS" in log_content assert "min intervals" in log_content @@ -512,7 +512,7 @@ def test_filtering_parameter_detide(self, test_data_setup): assert result is True # Check the log file contains the expected warning - log_files = list(setup["proc_dir"].glob("*_time_gridding.mooring.log")) + log_files = list(setup["proc_dir"].glob("processing_logs/*_time_gridding.log")) assert len(log_files) == 1 log_content = log_files[0].read_text()