ctdcast 0.1.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- ctdcast-0.1.0/LICENSE +21 -0
- ctdcast-0.1.0/PKG-INFO +192 -0
- ctdcast-0.1.0/README.md +148 -0
- ctdcast-0.1.0/ctdcast/__init__.py +25 -0
- ctdcast-0.1.0/ctdcast/__main__.py +6 -0
- ctdcast-0.1.0/ctdcast/_version.py +9 -0
- ctdcast-0.1.0/ctdcast/analysis/__init__.py +21 -0
- ctdcast-0.1.0/ctdcast/analysis/bathymetry.py +200 -0
- ctdcast-0.1.0/ctdcast/analysis/derive.py +407 -0
- ctdcast-0.1.0/ctdcast/analysis/geometry.py +68 -0
- ctdcast-0.1.0/ctdcast/cli/__init__.py +62 -0
- ctdcast-0.1.0/ctdcast/cli/convert.py +229 -0
- ctdcast-0.1.0/ctdcast/cli/draft.py +278 -0
- ctdcast-0.1.0/ctdcast/cli/init.py +1234 -0
- ctdcast-0.1.0/ctdcast/cli/process.py +392 -0
- ctdcast-0.1.0/ctdcast/cli/report.py +293 -0
- ctdcast-0.1.0/ctdcast/cli/run.py +164 -0
- ctdcast-0.1.0/ctdcast/cli/validate.py +244 -0
- ctdcast-0.1.0/ctdcast/config/__init__.py +1 -0
- ctdcast-0.1.0/ctdcast/config/loader.py +67 -0
- ctdcast-0.1.0/ctdcast/config/parameters.py +504 -0
- ctdcast-0.1.0/ctdcast/converters.py +27 -0
- ctdcast-0.1.0/ctdcast/ctdcast.mplstyle +24 -0
- ctdcast-0.1.0/ctdcast/identity.py +132 -0
- ctdcast-0.1.0/ctdcast/plotters/__init__.py +1 -0
- ctdcast-0.1.0/ctdcast/plotters/plots.py +1815 -0
- ctdcast-0.1.0/ctdcast/processors/__init__.py +129 -0
- ctdcast-0.1.0/ctdcast/processors/profiles.py +380 -0
- ctdcast-0.1.0/ctdcast/processors/qc.py +113 -0
- ctdcast-0.1.0/ctdcast/processors/stage1.py +213 -0
- ctdcast-0.1.0/ctdcast/processors/stage2.py +346 -0
- ctdcast-0.1.0/ctdcast/processors/stage3.py +138 -0
- ctdcast-0.1.0/ctdcast/readers/__init__.py +1 -0
- ctdcast-0.1.0/ctdcast/readers/cnv.py +50 -0
- ctdcast-0.1.0/ctdcast/readers/ladcp.py +55 -0
- ctdcast-0.1.0/ctdcast/readers/metadata.py +101 -0
- ctdcast-0.1.0/ctdcast/reports/__init__.py +5 -0
- ctdcast-0.1.0/ctdcast/reports/_cast.py +278 -0
- ctdcast-0.1.0/ctdcast/reports/_chrome.py +28 -0
- ctdcast-0.1.0/ctdcast/reports/_css.py +120 -0
- ctdcast-0.1.0/ctdcast/reports/_env.py +45 -0
- ctdcast-0.1.0/ctdcast/reports/_format.py +34 -0
- ctdcast-0.1.0/ctdcast/reports/_index.py +1192 -0
- ctdcast-0.1.0/ctdcast/reports/_leaflet.py +544 -0
- ctdcast-0.1.0/ctdcast/reports/_plots.py +655 -0
- ctdcast-0.1.0/ctdcast/reports/_section.py +394 -0
- ctdcast-0.1.0/ctdcast/reports/_timeseries.py +335 -0
- ctdcast-0.1.0/ctdcast/reports/leaflet/leaflet.min.css +8 -0
- ctdcast-0.1.0/ctdcast/reports/leaflet/leaflet.min.js +12 -0
- ctdcast-0.1.0/ctdcast/reports/templates/base.html +38 -0
- ctdcast-0.1.0/ctdcast/reports/templates/cast.html +203 -0
- ctdcast-0.1.0/ctdcast/reports/templates/casts.html +100 -0
- ctdcast-0.1.0/ctdcast/reports/templates/index.html +73 -0
- ctdcast-0.1.0/ctdcast/reports/templates/leaflet.html +165 -0
- ctdcast-0.1.0/ctdcast/reports/templates/section.html +147 -0
- ctdcast-0.1.0/ctdcast/reports/templates/sections.html +76 -0
- ctdcast-0.1.0/ctdcast/reports/templates/timeseries.html +80 -0
- ctdcast-0.1.0/ctdcast/reports/templates/timeseries_page.html +136 -0
- ctdcast-0.1.0/ctdcast/writers/__init__.py +1 -0
- ctdcast-0.1.0/ctdcast/writers/netcdf.py +89 -0
- ctdcast-0.1.0/ctdcast.egg-info/PKG-INFO +192 -0
- ctdcast-0.1.0/ctdcast.egg-info/SOURCES.txt +80 -0
- ctdcast-0.1.0/ctdcast.egg-info/dependency_links.txt +1 -0
- ctdcast-0.1.0/ctdcast.egg-info/entry_points.txt +2 -0
- ctdcast-0.1.0/ctdcast.egg-info/requires.txt +18 -0
- ctdcast-0.1.0/ctdcast.egg-info/top_level.txt +1 -0
- ctdcast-0.1.0/pyproject.toml +75 -0
- ctdcast-0.1.0/setup.cfg +4 -0
- ctdcast-0.1.0/tests/test_analysis.py +245 -0
- ctdcast-0.1.0/tests/test_converters.py +98 -0
- ctdcast-0.1.0/tests/test_cruise_profiles.py +152 -0
- ctdcast-0.1.0/tests/test_derive_salinity.py +62 -0
- ctdcast-0.1.0/tests/test_identity.py +107 -0
- ctdcast-0.1.0/tests/test_map_leaflet.py +47 -0
- ctdcast-0.1.0/tests/test_plots.py +120 -0
- ctdcast-0.1.0/tests/test_processors.py +71 -0
- ctdcast-0.1.0/tests/test_qc.py +108 -0
- ctdcast-0.1.0/tests/test_readers_cnv.py +109 -0
- ctdcast-0.1.0/tests/test_stage1.py +114 -0
- ctdcast-0.1.0/tests/test_stage2.py +124 -0
- ctdcast-0.1.0/tests/test_station.py +144 -0
- ctdcast-0.1.0/tests/test_writers_netcdf.py +89 -0
ctdcast-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Eleanor Frajka-Williams
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
ctdcast-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ctdcast
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Self-contained HTML report generator for shipboard CTD and LADCP data
|
|
5
|
+
Author: Angel Ruiz-Angulo
|
|
6
|
+
Author-email: Eleanor Frajka-Williams <eleanorfrajka@gmail.com>
|
|
7
|
+
License: MIT
|
|
8
|
+
Project-URL: Homepage, https://github.com/ocean-uhh/ctdcast
|
|
9
|
+
Project-URL: Source, https://github.com/ocean-uhh/ctdcast
|
|
10
|
+
Project-URL: Documentation, https://ocean-uhh.github.io/ctdcast
|
|
11
|
+
Project-URL: Bug Tracker, https://github.com/ocean-uhh/ctdcast/issues
|
|
12
|
+
Keywords: oceanography,CTD,LADCP,shipboard,seawater,report
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Science/Research
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Topic :: Scientific/Engineering
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Visualization
|
|
24
|
+
Requires-Python: <3.14,>=3.10
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
License-File: LICENSE
|
|
27
|
+
Requires-Dist: gsw>=3.6
|
|
28
|
+
Requires-Dist: matplotlib>=3.7
|
|
29
|
+
Requires-Dist: numpy>=1.24
|
|
30
|
+
Requires-Dist: xarray>=2023.1
|
|
31
|
+
Requires-Dist: netcdf4>=1.6
|
|
32
|
+
Requires-Dist: jinja2>=3.1
|
|
33
|
+
Requires-Dist: pyyaml>=6.0
|
|
34
|
+
Requires-Dist: ruamel.yaml>=0.18
|
|
35
|
+
Requires-Dist: scipy>=1.9
|
|
36
|
+
Provides-Extra: docs
|
|
37
|
+
Requires-Dist: sphinx>=7.2; extra == "docs"
|
|
38
|
+
Requires-Dist: sphinx-rtd-theme>=2.0; extra == "docs"
|
|
39
|
+
Requires-Dist: myst-parser>=2.0; extra == "docs"
|
|
40
|
+
Provides-Extra: test
|
|
41
|
+
Requires-Dist: pytest>=8; extra == "test"
|
|
42
|
+
Requires-Dist: pytest-cov>=5; extra == "test"
|
|
43
|
+
Dynamic: license-file
|
|
44
|
+
|
|
45
|
+
# ctdcast
|
|
46
|
+
|
|
47
|
+
[](https://github.com/ocean-uhh/ctdcast/actions/workflows/tests.yml)
|
|
48
|
+
[](https://www.python.org)
|
|
49
|
+
[](https://opensource.org/licenses/MIT)
|
|
50
|
+
[](https://ocean-uhh.github.io/ctdcast/)
|
|
51
|
+
|
|
52
|
+
Self-contained HTML report generator for shipboard CTD and LADCP data.
|
|
53
|
+
Produces portable HTML files — all figures embedded as base64 PNGs, no external requests —
|
|
54
|
+
for three report types: per-cast station pages, transect section pages, and a cruise-wide
|
|
55
|
+
time series page.
|
|
56
|
+
|
|
57
|
+
Designed for use at sea where internet connectivity is limited or absent.
|
|
58
|
+
All output files are fully self-contained and work offline.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Install
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
pip install ctdcast
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
For development (editable install from source):
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
git clone https://github.com/ocean-uhh/ctdcast
|
|
72
|
+
cd ctdcast
|
|
73
|
+
python -m venv venv
|
|
74
|
+
source venv/bin/activate # macOS / Linux
|
|
75
|
+
# venv\Scripts\activate # Windows
|
|
76
|
+
pip install -e .
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Dependencies: `gsw`, `matplotlib`, `numpy`, `xarray`, `netcdf4`, `jinja2`, `pyyaml`, `scipy`
|
|
80
|
+
|
|
81
|
+
CTD conversion: `seasenselib` converts raw CNV files to the netCDF format expected by ctdcast (`ctdcast draft` or `ctdcast run --ctd`). Install with `pip install seasenselib`. Pre-converted netCDF files from other tools must match ctdcast's variable naming convention (see docs).
|
|
82
|
+
|
|
83
|
+
To verify the installation, run the bundled demo against the committed fixture casts:
|
|
84
|
+
|
|
85
|
+
```bash
|
|
86
|
+
ctdcast run config_demo.yaml # writes demo_report/index.html
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
For a full walkthrough see the [Quickstart guide](https://ocean-uhh.github.io/ctdcast/quickstart.html).
|
|
90
|
+
|
|
91
|
+
---
|
|
92
|
+
|
|
93
|
+
## Quick start
|
|
94
|
+
|
|
95
|
+
### Quick look (no config file needed)
|
|
96
|
+
|
|
97
|
+
Raw CNV files fresh off the instrument? One command gives you station pages + index + map:
|
|
98
|
+
|
|
99
|
+
```bash
|
|
100
|
+
ctdcast draft /path/to/cnv/ # generates ./ctd_draft/index.html
|
|
101
|
+
ctdcast draft /path/to/cnv/ out/ --cruise odb2026 # with cruise ID
|
|
102
|
+
ctdcast draft /path/to/cnv/ --dry-run # preview what would happen
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
Requires `seasenselib` for CNV conversion (`pip install seasenselib`).
|
|
106
|
+
|
|
107
|
+
### Full workflow (with config)
|
|
108
|
+
|
|
109
|
+
For sections, time series, and LADCP panels you need a `config.yaml`:
|
|
110
|
+
|
|
111
|
+
#### 1. Write a config
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
ctdcast init # writes a template config.yaml
|
|
115
|
+
ctdcast init --interactive # guided setup: prompts for paths and
|
|
116
|
+
# auto-detects sections/timeseries from profiles.nc
|
|
117
|
+
ctdcast validate config.yaml # check paths before the first run
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
#### 2. Generate
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
ctdcast run config.yaml # smart update — skips up-to-date pages
|
|
124
|
+
ctdcast run config.yaml --force # rebuild everything
|
|
125
|
+
ctdcast run config.yaml --cast 42 # rebuild one cast page
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
Open `<output.dir>/index.html` in any browser.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## Input data
|
|
133
|
+
|
|
134
|
+
| File | Description |
|
|
135
|
+
|---|---|
|
|
136
|
+
| `cnv_nc/*.nc` | Per-cast netCDF files, one per CTD cast |
|
|
137
|
+
| `profiles.nc` | Compiled profiles on a 1 dbar grid |
|
|
138
|
+
| `ctd_sections.yaml` | Section definitions — which casts belong to each transect |
|
|
139
|
+
|
|
140
|
+
### ctd_sections.yaml format
|
|
141
|
+
|
|
142
|
+
```yaml
|
|
143
|
+
sections:
|
|
144
|
+
KTout:
|
|
145
|
+
description: "Kögur Transect outflow"
|
|
146
|
+
color: "#e41a1c"
|
|
147
|
+
cast_numbers: [[1, 12], 15] # ranges and/or individual cast numbers
|
|
148
|
+
FARDWO:
|
|
149
|
+
description: "FARDWO mooring array"
|
|
150
|
+
color: "#377eb8"
|
|
151
|
+
cast_numbers: [[20, 35], "22b"] # add "NNNb" to include a lettered sibling cast
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
Cast numbers are kept in the order written. An integer or range selects the
|
|
155
|
+
plain casts; a lettered sibling event (from a ``NNNb`` / ``NNN_b`` file) is a
|
|
156
|
+
distinct cast and must be named explicitly as a quoted ``"NNNb"`` string.
|
|
157
|
+
|
|
158
|
+
---
|
|
159
|
+
|
|
160
|
+
## Output structure
|
|
161
|
+
|
|
162
|
+
```
|
|
163
|
+
<output.dir>/
|
|
164
|
+
index.html front page — map + stats + navigation
|
|
165
|
+
casts.html table of all casts (latest first)
|
|
166
|
+
sections.html section cards with links
|
|
167
|
+
timeseries.html T, S, O₂ vs time × pressure
|
|
168
|
+
casts/
|
|
169
|
+
cast_001.html
|
|
170
|
+
cast_002.html
|
|
171
|
+
...
|
|
172
|
+
sections/
|
|
173
|
+
section_KTout.html
|
|
174
|
+
...
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
Each station page shows: CT profile, T/S/σ₀ triple-axis profile, T-S diagram coloured by
|
|
178
|
+
O₂ saturation, auxiliary profiles (O₂, fluorescence, turbidity), N²/Turner-angle stability
|
|
179
|
+
panels, and a cruise-track map with the cast highlighted.
|
|
180
|
+
|
|
181
|
+
---
|
|
182
|
+
|
|
183
|
+
## GEBCO bathymetry
|
|
184
|
+
|
|
185
|
+
Maps show GEBCO 2025 bathymetry when `gebco_nc` is set in `config.yaml`.
|
|
186
|
+
The file (~8 GB) is not bundled. Maps render without bathymetry if the path is missing — not an error.
|
|
187
|
+
|
|
188
|
+
---
|
|
189
|
+
|
|
190
|
+
## Documentation
|
|
191
|
+
|
|
192
|
+
Full documentation: [ocean-uhh.github.io/ctdcast](https://ocean-uhh.github.io/ctdcast/)
|
ctdcast-0.1.0/README.md
ADDED
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
# ctdcast
|
|
2
|
+
|
|
3
|
+
[](https://github.com/ocean-uhh/ctdcast/actions/workflows/tests.yml)
|
|
4
|
+
[](https://www.python.org)
|
|
5
|
+
[](https://opensource.org/licenses/MIT)
|
|
6
|
+
[](https://ocean-uhh.github.io/ctdcast/)
|
|
7
|
+
|
|
8
|
+
Self-contained HTML report generator for shipboard CTD and LADCP data.
|
|
9
|
+
Produces portable HTML files — all figures embedded as base64 PNGs, no external requests —
|
|
10
|
+
for three report types: per-cast station pages, transect section pages, and a cruise-wide
|
|
11
|
+
time series page.
|
|
12
|
+
|
|
13
|
+
Designed for use at sea where internet connectivity is limited or absent.
|
|
14
|
+
All output files are fully self-contained and work offline.
|
|
15
|
+
|
|
16
|
+
---
|
|
17
|
+
|
|
18
|
+
## Install
|
|
19
|
+
|
|
20
|
+
```bash
|
|
21
|
+
pip install ctdcast
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
For development (editable install from source):
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
git clone https://github.com/ocean-uhh/ctdcast
|
|
28
|
+
cd ctdcast
|
|
29
|
+
python -m venv venv
|
|
30
|
+
source venv/bin/activate # macOS / Linux
|
|
31
|
+
# venv\Scripts\activate # Windows
|
|
32
|
+
pip install -e .
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Dependencies: `gsw`, `matplotlib`, `numpy`, `xarray`, `netcdf4`, `jinja2`, `pyyaml`, `scipy`
|
|
36
|
+
|
|
37
|
+
CTD conversion: `seasenselib` converts raw CNV files to the netCDF format expected by ctdcast (`ctdcast draft` or `ctdcast run --ctd`). Install with `pip install seasenselib`. Pre-converted netCDF files from other tools must match ctdcast's variable naming convention (see docs).
|
|
38
|
+
|
|
39
|
+
To verify the installation, run the bundled demo against the committed fixture casts:
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
ctdcast run config_demo.yaml # writes demo_report/index.html
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
For a full walkthrough see the [Quickstart guide](https://ocean-uhh.github.io/ctdcast/quickstart.html).
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
## Quick start
|
|
50
|
+
|
|
51
|
+
### Quick look (no config file needed)
|
|
52
|
+
|
|
53
|
+
Raw CNV files fresh off the instrument? One command gives you station pages + index + map:
|
|
54
|
+
|
|
55
|
+
```bash
|
|
56
|
+
ctdcast draft /path/to/cnv/ # generates ./ctd_draft/index.html
|
|
57
|
+
ctdcast draft /path/to/cnv/ out/ --cruise odb2026 # with cruise ID
|
|
58
|
+
ctdcast draft /path/to/cnv/ --dry-run # preview what would happen
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Requires `seasenselib` for CNV conversion (`pip install seasenselib`).
|
|
62
|
+
|
|
63
|
+
### Full workflow (with config)
|
|
64
|
+
|
|
65
|
+
For sections, time series, and LADCP panels you need a `config.yaml`:
|
|
66
|
+
|
|
67
|
+
#### 1. Write a config
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
ctdcast init # writes a template config.yaml
|
|
71
|
+
ctdcast init --interactive # guided setup: prompts for paths and
|
|
72
|
+
# auto-detects sections/timeseries from profiles.nc
|
|
73
|
+
ctdcast validate config.yaml # check paths before the first run
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
#### 2. Generate
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
ctdcast run config.yaml # smart update — skips up-to-date pages
|
|
80
|
+
ctdcast run config.yaml --force # rebuild everything
|
|
81
|
+
ctdcast run config.yaml --cast 42 # rebuild one cast page
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Open `<output.dir>/index.html` in any browser.
|
|
85
|
+
|
|
86
|
+
---
|
|
87
|
+
|
|
88
|
+
## Input data
|
|
89
|
+
|
|
90
|
+
| File | Description |
|
|
91
|
+
|---|---|
|
|
92
|
+
| `cnv_nc/*.nc` | Per-cast netCDF files, one per CTD cast |
|
|
93
|
+
| `profiles.nc` | Compiled profiles on a 1 dbar grid |
|
|
94
|
+
| `ctd_sections.yaml` | Section definitions — which casts belong to each transect |
|
|
95
|
+
|
|
96
|
+
### ctd_sections.yaml format
|
|
97
|
+
|
|
98
|
+
```yaml
|
|
99
|
+
sections:
|
|
100
|
+
KTout:
|
|
101
|
+
description: "Kögur Transect outflow"
|
|
102
|
+
color: "#e41a1c"
|
|
103
|
+
cast_numbers: [[1, 12], 15] # ranges and/or individual cast numbers
|
|
104
|
+
FARDWO:
|
|
105
|
+
description: "FARDWO mooring array"
|
|
106
|
+
color: "#377eb8"
|
|
107
|
+
cast_numbers: [[20, 35], "22b"] # add "NNNb" to include a lettered sibling cast
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Cast numbers are kept in the order written. An integer or range selects the
|
|
111
|
+
plain casts; a lettered sibling event (from a ``NNNb`` / ``NNN_b`` file) is a
|
|
112
|
+
distinct cast and must be named explicitly as a quoted ``"NNNb"`` string.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Output structure
|
|
117
|
+
|
|
118
|
+
```
|
|
119
|
+
<output.dir>/
|
|
120
|
+
index.html front page — map + stats + navigation
|
|
121
|
+
casts.html table of all casts (latest first)
|
|
122
|
+
sections.html section cards with links
|
|
123
|
+
timeseries.html T, S, O₂ vs time × pressure
|
|
124
|
+
casts/
|
|
125
|
+
cast_001.html
|
|
126
|
+
cast_002.html
|
|
127
|
+
...
|
|
128
|
+
sections/
|
|
129
|
+
section_KTout.html
|
|
130
|
+
...
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Each station page shows: CT profile, T/S/σ₀ triple-axis profile, T-S diagram coloured by
|
|
134
|
+
O₂ saturation, auxiliary profiles (O₂, fluorescence, turbidity), N²/Turner-angle stability
|
|
135
|
+
panels, and a cruise-track map with the cast highlighted.
|
|
136
|
+
|
|
137
|
+
---
|
|
138
|
+
|
|
139
|
+
## GEBCO bathymetry
|
|
140
|
+
|
|
141
|
+
Maps show GEBCO 2025 bathymetry when `gebco_nc` is set in `config.yaml`.
|
|
142
|
+
The file (~8 GB) is not bundled. Maps render without bathymetry if the path is missing — not an error.
|
|
143
|
+
|
|
144
|
+
---
|
|
145
|
+
|
|
146
|
+
## Documentation
|
|
147
|
+
|
|
148
|
+
Full documentation: [ocean-uhh.github.io/ctdcast](https://ocean-uhh.github.io/ctdcast/)
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""ctdcast — CTD data processing and reporting for shipboard oceanography.
|
|
2
|
+
|
|
3
|
+
Subpackages
|
|
4
|
+
-----------
|
|
5
|
+
analysis: TEOS-10, cast geometry, GEBCO bathymetry (pure computation).
|
|
6
|
+
processors: per-cast and cruise-level processing pipeline (all stages).
|
|
7
|
+
readers: format readers (LADCP ``.mat``, CNV shim, sensor metadata).
|
|
8
|
+
reports: HTML page builders and the public ``report()`` entry point.
|
|
9
|
+
writers: output writers (CF-compliant netCDF).
|
|
10
|
+
|
|
11
|
+
Public API
|
|
12
|
+
----------
|
|
13
|
+
report(nc_dir, profiles_path, section_yaml, out_dir, force=False)
|
|
14
|
+
stage1(cnv_dir, nc_dir, ...)
|
|
15
|
+
profiles(nc_dir, profiles_path, ...)
|
|
16
|
+
process(stage=None, *, proc_dir, force=False, **kw)
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from ctdcast._version import __version__
|
|
20
|
+
from ctdcast.processors import process
|
|
21
|
+
from ctdcast.processors.profiles import build_profiles as profiles
|
|
22
|
+
from ctdcast.processors.stage1 import stage1
|
|
23
|
+
from ctdcast.reports._index import report
|
|
24
|
+
|
|
25
|
+
__all__ = ["__version__", "process", "profiles", "report", "stage1"]
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
"""Package version, resolved from installed metadata."""
|
|
2
|
+
|
|
3
|
+
from importlib.metadata import PackageNotFoundError
|
|
4
|
+
from importlib.metadata import version as _pkg_version
|
|
5
|
+
|
|
6
|
+
try:
|
|
7
|
+
__version__: str = _pkg_version("ctdcast")
|
|
8
|
+
except PackageNotFoundError:
|
|
9
|
+
__version__ = "0.1.0.dev0"
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""Shared scientific analysis: TEOS-10 derived variables, cast geometry, GEBCO bathymetry."""
|
|
2
|
+
|
|
3
|
+
from ctdcast.analysis.derive import (
|
|
4
|
+
derive_AOU,
|
|
5
|
+
derive_CT,
|
|
6
|
+
derive_SA,
|
|
7
|
+
derive_salinity,
|
|
8
|
+
derive_sigma0,
|
|
9
|
+
derive_teos10,
|
|
10
|
+
derive_teos10_profiles,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"derive_AOU",
|
|
15
|
+
"derive_CT",
|
|
16
|
+
"derive_SA",
|
|
17
|
+
"derive_salinity",
|
|
18
|
+
"derive_sigma0",
|
|
19
|
+
"derive_teos10",
|
|
20
|
+
"derive_teos10_profiles",
|
|
21
|
+
]
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"""GEBCO bathymetry loading and interpolation.
|
|
2
|
+
|
|
3
|
+
Pure computation — no matplotlib, no HTML. GEBCO stores elevation as negative
|
|
4
|
+
below sea level; this module returns depth as positive below sea level
|
|
5
|
+
(``depth = -elevation``).
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from __future__ import annotations
|
|
9
|
+
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
import numpy as np
|
|
13
|
+
import xarray as xr
|
|
14
|
+
|
|
15
|
+
# Module-level numpy cache: populated by preload_gebco() at report start.
|
|
16
|
+
# Maps str(path) → (all_lons, all_lats, all_depth) numpy arrays for the cruise area.
|
|
17
|
+
# Once populated, load_gebco subsets with numpy (zero disk I/O).
|
|
18
|
+
_GEBCO_CACHE: dict[str, tuple[np.ndarray, np.ndarray, np.ndarray]] = {}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def preload_gebco(
|
|
22
|
+
path: Path,
|
|
23
|
+
lat_lo: float,
|
|
24
|
+
lat_hi: float,
|
|
25
|
+
lon_lo: float,
|
|
26
|
+
lon_hi: float,
|
|
27
|
+
margin: float = 1.0,
|
|
28
|
+
) -> bool:
|
|
29
|
+
"""Load a GEBCO region into memory once for the cruise area.
|
|
30
|
+
|
|
31
|
+
Call this at report-generation start with the full lat/lon extent of all
|
|
32
|
+
casts. Subsequent ``load_gebco`` calls then subset from numpy arrays
|
|
33
|
+
(no disk I/O) instead of reopening the file for every map figure.
|
|
34
|
+
|
|
35
|
+
Parameters
|
|
36
|
+
----------
|
|
37
|
+
path:
|
|
38
|
+
Path to GEBCO netCDF file.
|
|
39
|
+
lat_lo, lat_hi, lon_lo, lon_hi:
|
|
40
|
+
Bounding box of the cruise area.
|
|
41
|
+
margin:
|
|
42
|
+
Extra degrees around the bounding box. Default 1.0 deg.
|
|
43
|
+
|
|
44
|
+
Returns
|
|
45
|
+
-------
|
|
46
|
+
bool
|
|
47
|
+
True if the file was found and cached successfully.
|
|
48
|
+
"""
|
|
49
|
+
if not Path(path).exists():
|
|
50
|
+
return False
|
|
51
|
+
try:
|
|
52
|
+
bathy = xr.open_dataset(path, engine="netcdf4")
|
|
53
|
+
lon_dim = "lon" if "lon" in bathy.coords else "longitude"
|
|
54
|
+
lat_dim = "lat" if "lat" in bathy.coords else "latitude"
|
|
55
|
+
sub = bathy.sel(
|
|
56
|
+
{
|
|
57
|
+
lon_dim: slice(lon_lo - margin, lon_hi + margin),
|
|
58
|
+
lat_dim: slice(lat_lo - margin, lat_hi + margin),
|
|
59
|
+
}
|
|
60
|
+
)
|
|
61
|
+
lons = sub[lon_dim].values.copy()
|
|
62
|
+
lats = sub[lat_dim].values.copy()
|
|
63
|
+
depth = -sub["elevation"].values.copy() # GEBCO: negative = below sea level
|
|
64
|
+
bathy.close()
|
|
65
|
+
_GEBCO_CACHE[str(path)] = (lons, lats, depth)
|
|
66
|
+
return True
|
|
67
|
+
except Exception: # noqa: BLE001
|
|
68
|
+
return False
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
def load_gebco(
|
|
72
|
+
lat_lo: float,
|
|
73
|
+
lat_hi: float,
|
|
74
|
+
lon_lo: float,
|
|
75
|
+
lon_hi: float,
|
|
76
|
+
margin: float = 0.05,
|
|
77
|
+
path: Path | None = None,
|
|
78
|
+
) -> tuple[np.ndarray, np.ndarray, np.ndarray] | None:
|
|
79
|
+
"""Return a GEBCO subset as (lons, lats, depth_m) or None if unavailable.
|
|
80
|
+
|
|
81
|
+
If ``preload_gebco`` has been called for *path*, subsets from the in-memory
|
|
82
|
+
numpy cache (fast). Otherwise opens the file from disk (slow).
|
|
83
|
+
|
|
84
|
+
Parameters
|
|
85
|
+
----------
|
|
86
|
+
path:
|
|
87
|
+
Path to GEBCO_2025.nc. Pass ``plots.GEBCO_PATH`` from the caller.
|
|
88
|
+
Returns None if not provided or file not found.
|
|
89
|
+
"""
|
|
90
|
+
if path is None or not Path(path).exists():
|
|
91
|
+
return None
|
|
92
|
+
path_str = str(path)
|
|
93
|
+
try:
|
|
94
|
+
if path_str in _GEBCO_CACHE:
|
|
95
|
+
all_lons, all_lats, all_depth = _GEBCO_CACHE[path_str]
|
|
96
|
+
lon_mask = (all_lons >= lon_lo - margin) & (all_lons <= lon_hi + margin)
|
|
97
|
+
lat_mask = (all_lats >= lat_lo - margin) & (all_lats <= lat_hi + margin)
|
|
98
|
+
lons = all_lons[lon_mask]
|
|
99
|
+
lats = all_lats[lat_mask]
|
|
100
|
+
if lons.size == 0 or lats.size == 0:
|
|
101
|
+
return None
|
|
102
|
+
depth = all_depth[lat_mask][:, lon_mask]
|
|
103
|
+
return lons, lats, depth
|
|
104
|
+
# Fallback when preload_gebco was not called: open file directly.
|
|
105
|
+
bathy = xr.open_dataset(path, engine="netcdf4")
|
|
106
|
+
lon_dim = "lon" if "lon" in bathy.coords else "longitude"
|
|
107
|
+
lat_dim = "lat" if "lat" in bathy.coords else "latitude"
|
|
108
|
+
sub = bathy.sel(
|
|
109
|
+
{
|
|
110
|
+
lon_dim: slice(lon_lo - margin, lon_hi + margin),
|
|
111
|
+
lat_dim: slice(lat_lo - margin, lat_hi + margin),
|
|
112
|
+
}
|
|
113
|
+
)
|
|
114
|
+
lons = sub[lon_dim].values
|
|
115
|
+
lats = sub[lat_dim].values
|
|
116
|
+
depth = -sub["elevation"].values # GEBCO: negative = below sea level
|
|
117
|
+
bathy.close()
|
|
118
|
+
return lons, lats, depth
|
|
119
|
+
except Exception: # noqa: BLE001
|
|
120
|
+
return None
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def interpolate_bathy_at_casts(
|
|
124
|
+
lats: list[float],
|
|
125
|
+
lons: list[float],
|
|
126
|
+
path: Path | None = None,
|
|
127
|
+
) -> np.ndarray | None:
|
|
128
|
+
"""Return GEBCO water depth (m, positive below sea level) at each cast position.
|
|
129
|
+
|
|
130
|
+
Uses bilinear interpolation via xarray. Returns ``None`` if GEBCO is not
|
|
131
|
+
available or on any error. Land points (elevation > 0) are clamped to 0.
|
|
132
|
+
"""
|
|
133
|
+
if path is None or not Path(path).exists():
|
|
134
|
+
return None
|
|
135
|
+
try:
|
|
136
|
+
lats_arr = np.asarray(lats, dtype=float)
|
|
137
|
+
lons_arr = np.asarray(lons, dtype=float)
|
|
138
|
+
finite = np.isfinite(lats_arr) & np.isfinite(lons_arr)
|
|
139
|
+
if not finite.any():
|
|
140
|
+
return None
|
|
141
|
+
lat_lo = float(lats_arr[finite].min())
|
|
142
|
+
lat_hi = float(lats_arr[finite].max())
|
|
143
|
+
lon_lo = float(lons_arr[finite].min())
|
|
144
|
+
lon_hi = float(lons_arr[finite].max())
|
|
145
|
+
gebco = load_gebco(lat_lo, lat_hi, lon_lo, lon_hi, margin=0.1, path=path)
|
|
146
|
+
if gebco is None:
|
|
147
|
+
return None
|
|
148
|
+
lons_g, lats_g, depth_g = gebco
|
|
149
|
+
# depth_g shape: (N_lat, N_lon) — positive = below sea level
|
|
150
|
+
da = xr.DataArray(
|
|
151
|
+
depth_g,
|
|
152
|
+
dims=["lat", "lon"],
|
|
153
|
+
coords={"lat": lats_g, "lon": lons_g},
|
|
154
|
+
)
|
|
155
|
+
da = da.sortby("lat").sortby("lon")
|
|
156
|
+
result = da.interp(
|
|
157
|
+
lat=xr.DataArray(lats_arr, dims="n"),
|
|
158
|
+
lon=xr.DataArray(lons_arr, dims="n"),
|
|
159
|
+
method="linear",
|
|
160
|
+
)
|
|
161
|
+
return np.maximum(result.values.astype(float), 0.0)
|
|
162
|
+
except Exception: # noqa: BLE001
|
|
163
|
+
return None
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def dense_bathy_along_track(
|
|
167
|
+
lats: list[float],
|
|
168
|
+
lons: list[float],
|
|
169
|
+
x_vals: np.ndarray,
|
|
170
|
+
path: Path | None = None,
|
|
171
|
+
n_per_segment: int = 20,
|
|
172
|
+
) -> tuple[np.ndarray | None, np.ndarray | None]:
|
|
173
|
+
"""Return ``(dense_x, dense_depths)`` interpolated between cast positions.
|
|
174
|
+
|
|
175
|
+
Generates *n_per_segment* equally-spaced points along each segment between
|
|
176
|
+
consecutive casts, giving a smooth GEBCO bathymetry fill rather than the
|
|
177
|
+
stepped appearance produced by one sample per cast. Returns ``(None, None)``
|
|
178
|
+
when GEBCO is unavailable or fewer than two cast positions are supplied.
|
|
179
|
+
"""
|
|
180
|
+
if path is None or not Path(path).exists() or len(lats) < 2:
|
|
181
|
+
return None, None
|
|
182
|
+
try:
|
|
183
|
+
dense_lats: list[float] = []
|
|
184
|
+
dense_lons: list[float] = []
|
|
185
|
+
dense_x: list[float] = []
|
|
186
|
+
for i in range(len(lats) - 1):
|
|
187
|
+
t = np.linspace(0.0, 1.0, n_per_segment, endpoint=False)
|
|
188
|
+
dense_lats.extend((lats[i] + t * (lats[i + 1] - lats[i])).tolist())
|
|
189
|
+
dense_lons.extend((lons[i] + t * (lons[i + 1] - lons[i])).tolist())
|
|
190
|
+
dense_x.extend((x_vals[i] + t * (x_vals[i + 1] - x_vals[i])).tolist())
|
|
191
|
+
dense_lats.append(lats[-1])
|
|
192
|
+
dense_lons.append(lons[-1])
|
|
193
|
+
dense_x.append(float(x_vals[-1]))
|
|
194
|
+
|
|
195
|
+
depths = interpolate_bathy_at_casts(dense_lats, dense_lons, path=path)
|
|
196
|
+
if depths is None:
|
|
197
|
+
return None, None
|
|
198
|
+
return np.array(dense_x), depths
|
|
199
|
+
except Exception: # noqa: BLE001
|
|
200
|
+
return None, None
|