forcingkit 0.4.0__tar.gz → 0.6.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.
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.env.template +6 -2
- {forcingkit-0.4.0 → forcingkit-0.6.0}/PKG-INFO +1 -1
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/conf.py +1 -1
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/index.rst +10 -3
- forcingkit-0.6.0/docs/source/rivers.rst +177 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/roadmap.rst +43 -3
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/sources.rst +21 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/pyproject.toml +1 -1
- {forcingkit-0.4.0 → forcingkit-0.6.0}/service/forcingkit_serve/main.py +220 -0
- forcingkit-0.6.0/src/forcingkit/data/rivers/northeast_us.json +29 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/dispatcher.py +153 -0
- forcingkit-0.6.0/src/forcingkit/fetchers/coastwatch.py +661 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/necofs.py +116 -0
- forcingkit-0.6.0/src/forcingkit/fetchers/usgs.py +347 -0
- forcingkit-0.6.0/src/forcingkit/fetchers/usgs_rivers.py +625 -0
- forcingkit-0.6.0/tests/unit/test_coastwatch.py +301 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_necofs_parent.py +9 -0
- forcingkit-0.6.0/tests/unit/test_usgs.py +110 -0
- forcingkit-0.6.0/tests/unit/test_usgs_rivers.py +490 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/uv.lock +1 -1
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.agents/forcingkit.md +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.claude/CLAUDE.md +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.github/copilot-instructions.md +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.github/workflows/docs.yml +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.github/workflows/publish.yml +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.github/workflows/tests.yml +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.gitignore +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.markdownlint.yaml +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.pre-commit-config.yaml +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/.python-version +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/AGENTS.md +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/CONTRIBUTING.md +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/Dockerfile +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/LICENSE +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/README.md +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docker-compose.yml +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/Makefile +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/make.bat +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/requirements-docs.txt +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/_extra/CNAME +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/_static/custom.css +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/_static/forcingkit-noreaster-wind.gif +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/atmospheric_forcing.rst +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/fetchers.rst +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/hycom.rst +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/necofs.rst +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/docs/source/nyofs.rst +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/main.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/service/__init__.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/service/forcingkit_serve/__init__.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/service/forcingkit_serve/routers/bathymetry.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/service/forcingkit_serve/routers/plotly_api.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/service/forcingkit_serve/routers/removed.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/service/forcingkit_serve/routers/viewer.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/service/run_server.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/__init__.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/dbofs.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/erddap.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/hrrr.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/hrrr_atmosphere.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/hycom.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/hydrography.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/ndbc.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/noaa.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/fetchers/nyofs.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/settings.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/src/forcingkit/zarr_stream.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/static/app.js +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/static/favicon.ico +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/static/favicon.svg +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/static/index.html +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/static/logo.svg +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/static/preview3d.js +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/static/styles.css +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/__init__.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/integration/test_auth.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/integration/test_erddap_fetch.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/integration/test_nyofs_obc_fetch.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/integration/test_obc_offshore_nj.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/integration/test_ofs_archive_paths.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/integration/test_s3_roms_fetchers.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/scripts/inspect_dem.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/scripts/inspect_grib.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/scripts/inspect_zarr.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_dbofs.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_dispatcher.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_hrrr_atm.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_hrrr_idx.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_hycom.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_main.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_ndbc.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_noaa_currents.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_noaa_datum.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_nyofs.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_obc_donor_report.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_obc_pipeline.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_removed_routes.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_settings.py +0 -0
- {forcingkit-0.4.0 → forcingkit-0.6.0}/tests/unit/test_zarr_stream.py +0 -0
|
@@ -2,8 +2,12 @@
|
|
|
2
2
|
# Copy this file to .env and fill in your credentials.
|
|
3
3
|
# A docker-compose service can load it with env_file.
|
|
4
4
|
|
|
5
|
-
# No credentials are required
|
|
6
|
-
#
|
|
5
|
+
# No credentials are required: HRRR, NOAA OFS, NECOFS, HYCOM, NDBC, CO-OPS and the
|
|
6
|
+
# USGS Water Data APIs are all served without authentication.
|
|
7
|
+
|
|
8
|
+
# Optional USGS Water Data API key (free from https://api.waterdata.usgs.gov/signup/).
|
|
9
|
+
# Without it, river discharge requests still work at a lower hourly rate limit.
|
|
10
|
+
# USGS_API_KEY=
|
|
7
11
|
|
|
8
12
|
# Cache root for every store (default ~/.cache/forcingkit). The old name
|
|
9
13
|
# ECODATA_CACHE_CACHE_DIR is still read, with a warning, until the next release.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: forcingkit
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.6.0
|
|
4
4
|
Summary: Spatiotemporal forcing for computational Earth-system models: selects, regrids and serves model-ready time series with provenance.
|
|
5
5
|
Project-URL: Documentation, https://forcingkit.docs.lhzn.io
|
|
6
6
|
Project-URL: Issues, https://github.com/lhzn-io/forcingkit/issues
|
|
@@ -17,13 +17,15 @@ and serves model-ready time series, with a record of where every value came from
|
|
|
17
17
|
What it does
|
|
18
18
|
------------
|
|
19
19
|
|
|
20
|
-
A regional ocean model needs
|
|
21
|
-
"parent" ocean along its open boundaries,
|
|
20
|
+
A regional ocean model needs three things from outside its own domain: the state of a larger
|
|
21
|
+
"parent" ocean along its open boundaries, the atmosphere above its surface, and the rivers that
|
|
22
|
+
flow into it. The first two come from
|
|
22
23
|
operational and archive systems (NOAA's `HRRR <https://rapidrefresh.noaa.gov/hrrr/>`__, the NOAA
|
|
23
24
|
`Operational Forecast Systems <https://tidesandcurrents.noaa.gov/models.html>`__,
|
|
24
25
|
`NECOFS <https://fvcom.smast.umassd.edu/?p=20>`__, `HYCOM <https://www.hycom.org/>`__) that
|
|
25
26
|
differ in grid type, vertical coordinate, time convention, file layout and access protocol, and
|
|
26
|
-
that change those details without notice.
|
|
27
|
+
that change those details without notice. River discharge comes from stream gauges, which
|
|
28
|
+
measure flow upstream of the head of tide rather than at the mouth.
|
|
27
29
|
|
|
28
30
|
forcingkit is a FastAPI service that sits between those providers and model codes such as
|
|
29
31
|
`Oceananigans.jl <https://github.com/CliMA/Oceananigans.jl>`__ (for example through
|
|
@@ -61,6 +63,10 @@ What it serves
|
|
|
61
63
|
- HRRR surface fields (10 m wind, 2 m temperature and humidity, pressure, radiation,
|
|
62
64
|
precipitation) on a regular 0.03 degree grid, hourly, as schema ``hrrr-atm-v1``. See
|
|
63
65
|
:doc:`atmospheric_forcing`.
|
|
66
|
+
* - ``POST /api/v1/rivers``
|
|
67
|
+
- Freshwater discharge at each listed river mouth in the box, hourly, from the tide-free
|
|
68
|
+
`USGS <https://waterdata.usgs.gov/>`__ stream gauges upstream (chosen per request) scaled
|
|
69
|
+
to the drainage area at the mouth, as schema ``river-v1``. See :doc:`rivers`.
|
|
64
70
|
* - ``POST /api/v1/tide``
|
|
65
71
|
- Observed water level at a `NOAA CO-OPS <https://tidesandcurrents.noaa.gov/>`__ station, for
|
|
66
72
|
validation.
|
|
@@ -154,6 +160,7 @@ PyPI as ``forcingkit``.
|
|
|
154
160
|
nyofs
|
|
155
161
|
hycom
|
|
156
162
|
atmospheric_forcing
|
|
163
|
+
rivers
|
|
157
164
|
|
|
158
165
|
.. toctree::
|
|
159
166
|
:maxdepth: 1
|
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
River Discharge
|
|
2
|
+
===============
|
|
3
|
+
|
|
4
|
+
``POST /api/v1/rivers`` delivers hourly freshwater discharge at the mouth of every listed river
|
|
5
|
+
in a box (or at mouths passed with the request), for point or surface sources in an ocean model. A nested regional model otherwise
|
|
6
|
+
receives river water only through its open boundaries and initial state, so a river that enters
|
|
7
|
+
inside the domain is missing as a source and its freshwater decays over the run.
|
|
8
|
+
|
|
9
|
+
Method
|
|
10
|
+
------
|
|
11
|
+
|
|
12
|
+
The method is common practice for gauge-driven river forcing. The NOAA Operational Forecast
|
|
13
|
+
Systems map US Geological Survey real-time gauges onto model cells with a per-river scale factor
|
|
14
|
+
and fall back to climatology. Regional estuary models scale gauged flow to ungauged area by the
|
|
15
|
+
ratio of drainage areas.
|
|
16
|
+
|
|
17
|
+
1. **Gauges, derived for every request.** Only a river's mouth is listed. From its NHDPlus
|
|
18
|
+
outlet flowline, forcingkit asks the USGS `NLDI <https://waterdata.usgs.gov/blog/nldi-intro/>`__
|
|
19
|
+
for every USGS site upstream, then keeps a site if:
|
|
20
|
+
|
|
21
|
+
- USGS classes it as a stream site (``ST``). Tidal streams (``ST-TS``) and estuaries (``ES``)
|
|
22
|
+
are left out, because their flow reverses with the tide; on the Connecticut River that rules
|
|
23
|
+
out Hartford and Middle Haddam;
|
|
24
|
+
- it has a drainage area, and a discharge record (15-minute or daily) covering the window;
|
|
25
|
+
- it drains at least ``min_share`` of the mouth's area (default 0.01), so headwater brooks
|
|
26
|
+
drop out;
|
|
27
|
+
- no other kept site lies downstream of it, which leaves the lowest gauge on each branch.
|
|
28
|
+
|
|
29
|
+
The kept gauges then measure disjoint parts of the basin. The selection follows the window: a
|
|
30
|
+
gauge that opened in 2010 is used from 2010 on, and one that closed is dropped from then on.
|
|
31
|
+
2. **Discharge at the mouth.** The sum of the river's gauges, scaled by the drainage area at the
|
|
32
|
+
mouth over the gauged area:
|
|
33
|
+
|
|
34
|
+
.. math::
|
|
35
|
+
|
|
36
|
+
Q_\text{mouth} = \frac{A_\text{mouth}}{\sum_k A_k} \sum_k Q_k
|
|
37
|
+
|
|
38
|
+
Gauge areas come from the USGS monitoring-location records; the mouth's area is that of the
|
|
39
|
+
outlet's NLDI basin.
|
|
40
|
+
3. **Time base.** The 15-minute record from the USGS Water Data APIs (``continuous``), averaged to
|
|
41
|
+
hourly values centred on the hour. The store holds ``hours + 1`` records, from ``start_time``
|
|
42
|
+
to ``start_time + hours``.
|
|
43
|
+
4. **Gaps.** A gap of up to ``max_gap_hours`` (default 6) with values on both sides is
|
|
44
|
+
interpolated. A longer gap takes that day's mean from the ``daily`` record. A day with neither
|
|
45
|
+
is an error, unless ``allow_climatology`` is set; then it takes the USGS day-of-year median of
|
|
46
|
+
daily flow. Every filled hour is flagged in ``fill`` (0 measured, 1 interpolated, 2 daily
|
|
47
|
+
mean, 3 climatology median), and the store's ``provenance`` attribute counts them per river.
|
|
48
|
+
5. **Water properties.** Salinity is zero. Water temperature (``temperature``) comes from the first
|
|
49
|
+
gauge on the river that measures it (USGS parameter 00010), and is NaN where none does.
|
|
50
|
+
6. **Provisional data.** USGS values are provisional until approved, typically months later, and
|
|
51
|
+
provisional values are revised. A store containing any provisional value is marked
|
|
52
|
+
``provisional = True`` and is served from the cache for one day only; after that a request
|
|
53
|
+
rebuilds it.
|
|
54
|
+
7. **Travel time.** None by default. A mouth entry may set ``lag_hours``; gauge values are then
|
|
55
|
+
taken that many hours earlier.
|
|
56
|
+
|
|
57
|
+
Mouths
|
|
58
|
+
------
|
|
59
|
+
|
|
60
|
+
Mouths are listed in ``forcingkit/data/rivers/*.json`` and in any ``*.json`` file in the directory
|
|
61
|
+
named by ``FORCINGKIT_RIVER_MOUTHS_DIR``; a later entry with the same name replaces an earlier one.
|
|
62
|
+
A request may instead pass its own ``mouths``. Adding a river needs a mouth entry and no code or
|
|
63
|
+
release:
|
|
64
|
+
|
|
65
|
+
.. code-block:: json
|
|
66
|
+
|
|
67
|
+
{"mouths": [
|
|
68
|
+
{"name": "Connecticut", "lon": -72.3305, "lat": 41.2726, "comid": 7703046},
|
|
69
|
+
{"name": "Saugatuck", "lon": -73.36, "lat": 41.10, "trace_from": "USGS-01208990"}
|
|
70
|
+
]}
|
|
71
|
+
|
|
72
|
+
- ``name``, ``lon``, ``lat`` (required): the point places the mouth for the bbox test.
|
|
73
|
+
- ``comid``: the NHDPlus outlet flowline. Otherwise ``trace_from`` names any NLDI feature on the
|
|
74
|
+
river (a USGS site such as ``USGS-01184000``) and the outlet is the end of its downstream trace.
|
|
75
|
+
Failing both, the point is snapped to the nearest flowline, with a warning: at an estuary the
|
|
76
|
+
nearest flowline can be a short coastal one rather than the river.
|
|
77
|
+
- ``include`` and ``exclude``: USGS site numbers to pin or rule out where judgement beats the rule
|
|
78
|
+
(for example a gauge below a dam whose releases should not drive the forcing). A pinned set that
|
|
79
|
+
drains more than the mouth's area is refused as double counting.
|
|
80
|
+
- ``lag_hours``: travel time from the gauges to the mouth, applied to the gauge window.
|
|
81
|
+
|
|
82
|
+
The id of a store includes the mouth entries and ``min_share``, but not the derived gauges, which
|
|
83
|
+
are recorded in the store instead. NLDI facts about a mouth (its outlet, basin area, upstream sites
|
|
84
|
+
and which sites lie downstream of which) are cached for 30 days under ``usgs/derivations`` in the
|
|
85
|
+
cache directory, so only the first request for a mouth takes long (about a minute for the
|
|
86
|
+
Connecticut, whose basin holds 1,375 USGS sites).
|
|
87
|
+
|
|
88
|
+
The packaged mouths are the rivers into Long Island Sound. For a request over the Sound in April
|
|
89
|
+
2026 the derivation chose:
|
|
90
|
+
|
|
91
|
+
.. list-table::
|
|
92
|
+
:header-rows: 1
|
|
93
|
+
:widths: 16 46 13 13 12
|
|
94
|
+
|
|
95
|
+
* - River
|
|
96
|
+
- USGS gauges kept
|
|
97
|
+
- Gauged (mi²)
|
|
98
|
+
- Mouth (mi²)
|
|
99
|
+
- Scale
|
|
100
|
+
* - Housatonic
|
|
101
|
+
- 01205500 at Stevenson; 01208500 Naugatuck at Beacon Falls
|
|
102
|
+
- 1,804
|
|
103
|
+
- 1,944
|
|
104
|
+
- 1.08
|
|
105
|
+
* - Quinnipiac
|
|
106
|
+
- 01196500 at Wallingford; 01196620 Mill River near Hamden; 01196561 Muddy River near East
|
|
107
|
+
Wallingford
|
|
108
|
+
- 148
|
|
109
|
+
- 204
|
|
110
|
+
- 1.37
|
|
111
|
+
* - Connecticut
|
|
112
|
+
- 01184000 at Thompsonville; 01189995 Farmington at Tariffville
|
|
113
|
+
- 10,237
|
|
114
|
+
- 11,242
|
|
115
|
+
- 1.10
|
|
116
|
+
* - Thames
|
|
117
|
+
- 01127000 Quinebaug at Jewett City; 011230695 Shetucket at Taftville; 01127500 Yantic at
|
|
118
|
+
Yantic
|
|
119
|
+
- 1,314
|
|
120
|
+
- 1,470
|
|
121
|
+
- 1.12
|
|
122
|
+
|
|
123
|
+
The Quinnipiac's outlet basin is New Haven Harbor's, so its scaled flow also stands for the
|
|
124
|
+
harbour's other streams. The Housatonic at Stevenson is below a hydroelectric dam, so its hourly
|
|
125
|
+
flow follows the dam's releases. The Bronx River gauge (01302020) has no record before the 2000s,
|
|
126
|
+
so a request for an earlier window fails unless ``skip_ungauged`` is set, which leaves such a
|
|
127
|
+
river out and lists it in the store's ``skipped_rivers``.
|
|
128
|
+
|
|
129
|
+
Request and store
|
|
130
|
+
-----------------
|
|
131
|
+
|
|
132
|
+
.. code-block:: bash
|
|
133
|
+
|
|
134
|
+
curl -s -X POST localhost:9598/api/v1/rivers -H 'Content-Type: application/json' -d '{
|
|
135
|
+
"bbox": {"min_lon": -73.855, "min_lat": 40.75, "max_lon": -71.9, "max_lat": 41.45},
|
|
136
|
+
"start_time": "2026-04-02T00:00:00Z", "hours": 48}'
|
|
137
|
+
|
|
138
|
+
Optional fields: ``mouths``, ``min_share`` (default 0.01), ``max_gap_hours`` (default 6),
|
|
139
|
+
``allow_climatology`` and ``skip_ungauged`` (both default false), and ``cache_bust``.
|
|
140
|
+
|
|
141
|
+
For that request (four rivers, 49 records), the mean discharges over the 48 hours were 1,553 m³/s
|
|
142
|
+
for the Connecticut, 147 for the Housatonic, 148 for the Thames and 13 for the Quinnipiac.
|
|
143
|
+
|
|
144
|
+
The store (schema ``river-v1``) has a ``river`` dimension:
|
|
145
|
+
|
|
146
|
+
- ``discharge`` (time, river): m³ s⁻¹, ``water_volume_transport_in_river_channel``.
|
|
147
|
+
- ``temperature`` (time, river): °C.
|
|
148
|
+
- ``fill`` (time, river): the flags above.
|
|
149
|
+
- ``mouth_lon``, ``mouth_lat``, ``scale_factor``, ``salinity`` (river).
|
|
150
|
+
- Attributes: ``river_names``; ``provenance`` (JSON: per river, the gauges kept, the areas, the
|
|
151
|
+
scale factor, the outlet, and the dropped candidates counted by reason); ``derivation_dropped``
|
|
152
|
+
(JSON: every dropped candidate with its reason); ``skipped_rivers``; ``provisional``; and
|
|
153
|
+
``built_utc``.
|
|
154
|
+
|
|
155
|
+
A JSON sidecar next to the store repeats the request, the mouths and the derivation. ``GET /api/v1/rivers/download/{zarr_id}``
|
|
156
|
+
returns the store as a zip.
|
|
157
|
+
|
|
158
|
+
Credentials
|
|
159
|
+
-----------
|
|
160
|
+
|
|
161
|
+
None are required. A free USGS Water Data API key (``USGS_API_KEY``, from
|
|
162
|
+
`api.waterdata.usgs.gov/signup <https://api.waterdata.usgs.gov/signup/>`__) raises the hourly rate
|
|
163
|
+
limit; it is sent in the ``X-Api-Key`` header. forcingkit uses the modernized APIs, not the
|
|
164
|
+
legacy ``waterservices.usgs.gov``, which USGS plans to retire in 2027.
|
|
165
|
+
|
|
166
|
+
Limits
|
|
167
|
+
------
|
|
168
|
+
|
|
169
|
+
- Mouths are not found automatically within a box: each river needs a mouth entry. Finding
|
|
170
|
+
coastal outlets from NHDPlus would remove that step.
|
|
171
|
+
- The selection rule cannot tell a regulated gauge from a natural one; use ``exclude`` where dam
|
|
172
|
+
operations should not reach the forcing.
|
|
173
|
+
- Gauges measure surface runoff. Groundwater discharge (most of Long Island's freshwater input) is
|
|
174
|
+
not represented.
|
|
175
|
+
- A model that applies discharge as a surface freshwater flux on fixed levels dilutes salinity
|
|
176
|
+
but adds no volume. Whether the volume is added depends on the model's vertical coordinate, not
|
|
177
|
+
on this store.
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
Roadmap and Contributing
|
|
2
2
|
========================
|
|
3
3
|
|
|
4
|
-
As of 2026-10-
|
|
5
|
-
ocean** for a model's open boundaries (``/api/v1/obc``)
|
|
6
|
-
(``/api/v1/atmosphere``). New sources are ranked by
|
|
4
|
+
As of 2026-10-09. This page covers the kinds of forcing forcingkit delivers: the **parent
|
|
5
|
+
ocean** for a model's open boundaries (``/api/v1/obc``), the **surface atmosphere**
|
|
6
|
+
(``/api/v1/atmosphere``) and **river discharge** (``/api/v1/rivers``). New sources are ranked by
|
|
7
|
+
two questions:
|
|
7
8
|
|
|
8
9
|
1. **Coverage first.** Does it serve a region, or a span of years, that no current source does?
|
|
9
10
|
2. **Resolution second.** Where something already serves, does it resolve the coast better?
|
|
@@ -44,6 +45,10 @@ Current coverage
|
|
|
44
45
|
- Contiguous US (atmosphere)
|
|
45
46
|
- 2014-07-30 on
|
|
46
47
|
- 3 km
|
|
48
|
+
* - :doc:`USGS gauges <rivers>`
|
|
49
|
+
- Rivers into Long Island Sound (river discharge)
|
|
50
|
+
- 15-minute values online from about 2007; daily means fill in further back
|
|
51
|
+
- Five river mouths
|
|
47
52
|
|
|
48
53
|
So today the only parent ocean outside the US Northeast is HYCOM, at 1/12 degree and in the
|
|
49
54
|
legacy output layout, and there is no atmosphere outside the contiguous US: there, and before
|
|
@@ -73,6 +78,41 @@ Planned work
|
|
|
73
78
|
hindcasts (:doc:`atmospheric_forcing`).
|
|
74
79
|
* - Cache policy
|
|
75
80
|
- Invalidate stores built from forecasts when the upstream system publishes a newer cycle.
|
|
81
|
+
* - A second global parent
|
|
82
|
+
- HYCOM is the only parent ocean for current dates outside the US Northeast, and its server
|
|
83
|
+
(``tds.hycom.org``) often stalls past the read timeout (:doc:`hycom`). Copernicus GLO12
|
|
84
|
+
(see `Global parent ocean`_) would give an independent fallback for 2020-11-01 on.
|
|
85
|
+
* - Warn on gaps in a donor's record
|
|
86
|
+
- Every HYCOM experiment has a few gaps of up to 51 hours in its 3-hourly record, and the
|
|
87
|
+
hourly resampling bridges them by linear interpolation without notice. Log a warning,
|
|
88
|
+
and record the gap in the store's attributes, when a window contains a step longer than
|
|
89
|
+
about 6 hours.
|
|
90
|
+
* - HYCOM fetcher clean-up
|
|
91
|
+
- Remove the unused ``_normalize_lons``. Select the initial-condition time by position, as
|
|
92
|
+
the boundary path does: the nearest-label lookup fails on the non-monotonic time axis of
|
|
93
|
+
GLBv0.08 ``expt_93.0``.
|
|
94
|
+
* - River mouths for more regions, and found automatically
|
|
95
|
+
- :doc:`rivers` derives each river's gauges per request, so a new river needs only a mouth
|
|
96
|
+
entry. Mouths for the Hudson, Raritan and Delaware would cover the NY Bight and Delaware Bay
|
|
97
|
+
parents already served; finding coastal outlets in a box from NHDPlus would remove the
|
|
98
|
+
entries altogether.
|
|
99
|
+
* - Ungauged rivers
|
|
100
|
+
- Where no tide-free gauge exists, the `National Water Model
|
|
101
|
+
<https://water.noaa.gov/about/nwm>`__ retrospective and operational streamflow (on AWS)
|
|
102
|
+
could supply discharge at the outlet reach.
|
|
103
|
+
* - mypy hook without an active environment
|
|
104
|
+
- The ``mypy`` pre-commit hook runs the ``mypy`` on ``PATH``, so a commit made without the
|
|
105
|
+
project environment activated fails with "Executable ``mypy`` not found". Run it through
|
|
106
|
+
``uv run`` so it works from any shell.
|
|
107
|
+
* - Fetcher module names
|
|
108
|
+
- Modules in ``forcingkit.fetchers`` are named for their source (``necofs``, ``hycom``,
|
|
109
|
+
``ndbc``, ``coastwatch``, ``usgs``), and a model-ready delivery for the source and what it
|
|
110
|
+
delivers (``hrrr_atmosphere``, ``usgs_rivers``). Three predate the convention:
|
|
111
|
+
``noaa.py`` serves CO-OPS water levels and current predictions (``coops.py``),
|
|
112
|
+
``erddap.py`` serves the UConn profile stations (named for the protocol, not the source),
|
|
113
|
+
and ``hydrography.py`` finds heads of tide from OpenStreetMap (``osm.py`` or similar).
|
|
114
|
+
Renaming them breaks imports from earlier releases, so it belongs in a release that says
|
|
115
|
+
so, with the old names kept as deprecated aliases for one release.
|
|
76
116
|
|
|
77
117
|
Candidate sources: coverage
|
|
78
118
|
---------------------------
|
|
@@ -49,6 +49,21 @@ Providers
|
|
|
49
49
|
* - NDBC buoys
|
|
50
50
|
- `NOAA National Data Buoy Center <https://www.ndbc.noaa.gov/>`__
|
|
51
51
|
- NOAA attribution.
|
|
52
|
+
* - River discharge
|
|
53
|
+
- `US Geological Survey <https://www.usgs.gov/>`__ stream gauges, through the `USGS Water
|
|
54
|
+
Data APIs <https://api.waterdata.usgs.gov/>`__; drainage areas from the USGS `NLDI
|
|
55
|
+
<https://waterdata.usgs.gov/blog/nldi-intro/>`__ and NHDPlus
|
|
56
|
+
- Cite the gauges as "U.S. Geological Survey, USGS Water Data for the Nation", with the site
|
|
57
|
+
numbers in the store's ``provenance``. USGS data are in the public domain; provisional
|
|
58
|
+
values are subject to revision.
|
|
59
|
+
* - Satellite surface fields
|
|
60
|
+
- `NOAA NESDIS CoastWatch <https://coastwatch.noaa.gov/>`__ ERDDAP servers: VIIRS
|
|
61
|
+
chlorophyll-a and Kd490 (S-NPP, NOAA-20, NOAA-21), Sentinel-3 OLCI chlorophyll-a, and the
|
|
62
|
+
gap-filled DINEOF products; MUR sea surface temperature from `NASA JPL
|
|
63
|
+
<https://podaac.jpl.nasa.gov/>`__ (PO.DAAC), served through CoastWatch
|
|
64
|
+
- Acknowledge NOAA NESDIS CoastWatch and the datasets named in the store's ``products``
|
|
65
|
+
attribute. MUR: JPL MUR MEaSUREs Project (2015) and Chin et al. (2017). OLCI and DINEOF
|
|
66
|
+
products require "Contains modified Copernicus Sentinel data [year]".
|
|
52
67
|
* - Water-column profiles
|
|
53
68
|
- `University of Connecticut Department of Marine Sciences
|
|
54
69
|
<https://marinesciences.uconn.edu/>`__ (`ERDDAP
|
|
@@ -91,6 +106,9 @@ References
|
|
|
91
106
|
estuaries. *Journal of Atmospheric and Oceanic Technology*, 20(1), 159-186.
|
|
92
107
|
`doi:10.1175/1520-0426(2003)020<0159:AUGFVT>2.0.CO;2
|
|
93
108
|
<https://doi.org/10.1175/1520-0426(2003)020%3C0159:AUGFVT%3E2.0.CO;2>`__
|
|
109
|
+
- Chin, T. M., J. Vazquez-Cuervo and E. M. Armstrong (2017). A multi-scale high-resolution
|
|
110
|
+
analysis of global sea surface temperature. *Remote Sensing of Environment*, 200, 154-169.
|
|
111
|
+
`doi:10.1016/j.rse.2017.07.029 <https://doi.org/10.1016/j.rse.2017.07.029>`__
|
|
94
112
|
- Dowell, D. C., C. R. Alexander, E. P. James, et al. (2022). The High-Resolution Rapid Refresh
|
|
95
113
|
(HRRR): An hourly updating convection-allowing forecast model. Part I: Motivation and system
|
|
96
114
|
description. *Weather and Forecasting*, 37(8), 1371-1395.
|
|
@@ -102,6 +120,9 @@ References
|
|
|
102
120
|
(HRRR): An hourly updating convection-allowing forecast model. Part II: Forecast performance.
|
|
103
121
|
*Weather and Forecasting*, 37(8), 1397-1417.
|
|
104
122
|
`doi:10.1175/WAF-D-21-0130.1 <https://doi.org/10.1175/WAF-D-21-0130.1>`__
|
|
123
|
+
- JPL MUR MEaSUREs Project (2015). GHRSST Level 4 MUR Global Foundation Sea Surface
|
|
124
|
+
Temperature Analysis (v4.1). PO.DAAC, CA, USA.
|
|
125
|
+
`doi:10.5067/GHGMR-4FJ04 <https://doi.org/10.5067/GHGMR-4FJ04>`__
|
|
105
126
|
- NOAA (2002). *Implementation Plan, Port of New York and New Jersey Operational Forecast System
|
|
106
127
|
(NYOFS)*. NOAA Technical Report NOS CO-OPS 37, Silver Spring, MD.
|
|
107
128
|
`PDF <https://tidesandcurrents.noaa.gov/publications/techrpt37.pdf>`__
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "forcingkit"
|
|
7
|
-
version = "0.
|
|
7
|
+
version = "0.6.0"
|
|
8
8
|
description = "Spatiotemporal forcing for computational Earth-system models: selects, regrids and serves model-ready time series with provenance."
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -589,6 +589,226 @@ def download_atmosphere(zarr_id: str):
|
|
|
589
589
|
)
|
|
590
590
|
|
|
591
591
|
|
|
592
|
+
class RiverRequest(BaseModel):
|
|
593
|
+
bbox: BoundingBox
|
|
594
|
+
start_time: str # ISO 8601, UTC
|
|
595
|
+
hours: int
|
|
596
|
+
max_gap_hours: int = 6
|
|
597
|
+
allow_climatology: bool = False
|
|
598
|
+
# Mouths to serve instead of the listed ones in the bbox: each {name, lon, lat} with
|
|
599
|
+
# optional comid or trace_from (the outlet), include / exclude (USGS site numbers) and
|
|
600
|
+
# lag_hours.
|
|
601
|
+
mouths: List[Dict[str, Any]] | None = None
|
|
602
|
+
# Gauges draining less than this share of the mouth's area are left out.
|
|
603
|
+
min_share: float = 0.01
|
|
604
|
+
# Leave out, rather than fail on, a river with no gauge record over the window.
|
|
605
|
+
skip_ungauged: bool = False
|
|
606
|
+
cache_bust: bool = False
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
def _river_cache_dir() -> str:
|
|
610
|
+
return settings.cache_dir("rivers")
|
|
611
|
+
|
|
612
|
+
|
|
613
|
+
@app.post("/api/v1/rivers")
|
|
614
|
+
def generate_rivers(request: RiverRequest) -> Dict[str, Any]:
|
|
615
|
+
"""Hourly discharge at each river mouth in the bbox (or each of `mouths`), for point sources
|
|
616
|
+
in an ocean model: the lowest tide-free USGS gauge on each branch, derived per request from
|
|
617
|
+
USGS NLDI and site metadata, summed and scaled by drainage area, from `start_time` to
|
|
618
|
+
`start_time + hours`, streamed to a Zarr store."""
|
|
619
|
+
import json
|
|
620
|
+
|
|
621
|
+
import zarr
|
|
622
|
+
|
|
623
|
+
from forcingkit.dispatcher import dispatch_river_request, river_key, river_mouths
|
|
624
|
+
|
|
625
|
+
if request.hours < 1:
|
|
626
|
+
raise HTTPException(status_code=400, detail="hours must be at least 1")
|
|
627
|
+
if request.max_gap_hours < 0:
|
|
628
|
+
raise HTTPException(status_code=400, detail="max_gap_hours must be at least 0")
|
|
629
|
+
if not 0 <= request.min_share < 1:
|
|
630
|
+
raise HTTPException(status_code=400, detail="min_share must be in [0, 1)")
|
|
631
|
+
bbox_list = [
|
|
632
|
+
request.bbox.min_lon,
|
|
633
|
+
request.bbox.min_lat,
|
|
634
|
+
request.bbox.max_lon,
|
|
635
|
+
request.bbox.max_lat,
|
|
636
|
+
]
|
|
637
|
+
try:
|
|
638
|
+
mouths = river_mouths(bbox_list, request.mouths)
|
|
639
|
+
except ValueError as e:
|
|
640
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
641
|
+
if not mouths:
|
|
642
|
+
raise HTTPException(
|
|
643
|
+
status_code=400, detail=f"no listed river mouth lies in bbox {bbox_list}"
|
|
644
|
+
)
|
|
645
|
+
zarr_id = river_key(
|
|
646
|
+
bbox_list,
|
|
647
|
+
request.start_time,
|
|
648
|
+
request.hours,
|
|
649
|
+
request.max_gap_hours,
|
|
650
|
+
request.allow_climatology,
|
|
651
|
+
mouths,
|
|
652
|
+
request.min_share,
|
|
653
|
+
request.skip_ungauged,
|
|
654
|
+
)
|
|
655
|
+
os.makedirs(_river_cache_dir(), exist_ok=True)
|
|
656
|
+
zarr_path = os.path.join(_river_cache_dir(), f"{zarr_id}.zarr")
|
|
657
|
+
try:
|
|
658
|
+
final_path = dispatch_river_request(
|
|
659
|
+
bbox_list,
|
|
660
|
+
request.start_time,
|
|
661
|
+
request.hours,
|
|
662
|
+
zarr_path,
|
|
663
|
+
max_gap_hours=request.max_gap_hours,
|
|
664
|
+
allow_climatology=request.allow_climatology,
|
|
665
|
+
cache_bust=request.cache_bust,
|
|
666
|
+
mouths=mouths,
|
|
667
|
+
min_share=request.min_share,
|
|
668
|
+
skip_ungauged=request.skip_ungauged,
|
|
669
|
+
)
|
|
670
|
+
except ValueError as e:
|
|
671
|
+
raise HTTPException(status_code=400, detail=str(e))
|
|
672
|
+
except Exception as e:
|
|
673
|
+
logger.error(f"River delivery failed: {e}")
|
|
674
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
675
|
+
attrs = dict(zarr.open_group(final_path, mode="r", zarr_format=2).attrs)
|
|
676
|
+
return {
|
|
677
|
+
"status": "success",
|
|
678
|
+
"zarr_id": zarr_id,
|
|
679
|
+
"zarr_path": final_path,
|
|
680
|
+
"download_url": f"/api/v1/rivers/download/{zarr_id}",
|
|
681
|
+
"rivers": json.loads(str(attrs.get("provenance", "[]"))),
|
|
682
|
+
"provisional": bool(attrs.get("provisional", False)),
|
|
683
|
+
"skipped_rivers": json.loads(str(attrs.get("skipped_rivers", "[]"))),
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
|
|
687
|
+
@app.get("/api/v1/rivers/download/{zarr_id}")
|
|
688
|
+
def download_rivers(zarr_id: str):
|
|
689
|
+
search_id = zarr_id if zarr_id.startswith("riv_") else f"riv_{zarr_id}"
|
|
690
|
+
if not search_id[4:].isalnum():
|
|
691
|
+
raise HTTPException(status_code=400, detail="invalid store id")
|
|
692
|
+
cache_dir = _river_cache_dir()
|
|
693
|
+
zarr_path = os.path.join(cache_dir, f"{search_id}.zarr")
|
|
694
|
+
if not os.path.isdir(zarr_path):
|
|
695
|
+
raise HTTPException(status_code=404, detail="River Zarr store not found.")
|
|
696
|
+
zip_path = os.path.join(cache_dir, f"{search_id}.zip")
|
|
697
|
+
if _zip_is_stale(zip_path, zarr_path):
|
|
698
|
+
shutil.make_archive(zip_path.replace(".zip", ""), "zip", zarr_path)
|
|
699
|
+
return FileResponse(
|
|
700
|
+
zip_path, media_type="application/zip", filename=f"{search_id}.zip"
|
|
701
|
+
)
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
class SatelliteRequest(BaseModel):
|
|
705
|
+
bbox: BoundingBox
|
|
706
|
+
quantity: (
|
|
707
|
+
str # "sst", "chlor_a", "kd_490" or "spm" (see fetchers.coastwatch.QUANTITIES)
|
|
708
|
+
)
|
|
709
|
+
end_time: str | None = None # ISO 8601, UTC; default now
|
|
710
|
+
days: int = 7
|
|
711
|
+
coast_pixels: int = 1
|
|
712
|
+
cache_bust: bool = False
|
|
713
|
+
|
|
714
|
+
|
|
715
|
+
def _satellite_cache_dir() -> str:
|
|
716
|
+
return settings.cache_dir("satellite")
|
|
717
|
+
|
|
718
|
+
|
|
719
|
+
def _satellite_retention_days() -> float:
|
|
720
|
+
return float(settings.env("FORCINGKIT_SATELLITE_RETENTION_DAYS", "30") or 30)
|
|
721
|
+
|
|
722
|
+
|
|
723
|
+
@app.post("/api/v1/satellite")
|
|
724
|
+
def get_satellite(request: SatelliteRequest) -> Dict[str, Any]:
|
|
725
|
+
"""A satellite surface field over the bbox: each pixel's most recent valid value across the quantity's sensors
|
|
726
|
+
within `days` of `end_time`, with its age, a band of `coast_pixels` masked along the shore, as a Zarr store.
|
|
727
|
+
|
|
728
|
+
Stores older than FORCINGKIT_SATELLITE_RETENTION_DAYS (default 30) are pruned on every request, so the cache
|
|
729
|
+
stays bounded.
|
|
730
|
+
"""
|
|
731
|
+
import json
|
|
732
|
+
from datetime import datetime, timezone
|
|
733
|
+
|
|
734
|
+
import pandas as pd
|
|
735
|
+
|
|
736
|
+
from forcingkit.fetchers import coastwatch
|
|
737
|
+
|
|
738
|
+
products = coastwatch.QUANTITIES.get(request.quantity)
|
|
739
|
+
if products is None:
|
|
740
|
+
raise HTTPException(
|
|
741
|
+
status_code=400,
|
|
742
|
+
detail=f"quantity must be one of {sorted(coastwatch.QUANTITIES)}",
|
|
743
|
+
)
|
|
744
|
+
if not 1 <= request.days <= 31:
|
|
745
|
+
raise HTTPException(status_code=400, detail="days must be 1 to 31")
|
|
746
|
+
end = (
|
|
747
|
+
pd.Timestamp(request.end_time).to_pydatetime()
|
|
748
|
+
if request.end_time
|
|
749
|
+
else datetime.now(timezone.utc)
|
|
750
|
+
)
|
|
751
|
+
bbox_list = [
|
|
752
|
+
request.bbox.min_lon,
|
|
753
|
+
request.bbox.min_lat,
|
|
754
|
+
request.bbox.max_lon,
|
|
755
|
+
request.bbox.max_lat,
|
|
756
|
+
]
|
|
757
|
+
cache_dir = _satellite_cache_dir()
|
|
758
|
+
os.makedirs(cache_dir, exist_ok=True)
|
|
759
|
+
coastwatch.prune_cache(cache_dir, _satellite_retention_days())
|
|
760
|
+
zarr_id = coastwatch.cache_key(
|
|
761
|
+
products, bbox_list, end, request.days, request.coast_pixels
|
|
762
|
+
)
|
|
763
|
+
zarr_path = os.path.join(cache_dir, f"{zarr_id}.zarr")
|
|
764
|
+
if request.cache_bust or not os.path.isdir(zarr_path):
|
|
765
|
+
try:
|
|
766
|
+
ds = coastwatch.mask_coast(
|
|
767
|
+
coastwatch.composite(products, bbox_list, end, request.days),
|
|
768
|
+
request.coast_pixels,
|
|
769
|
+
)
|
|
770
|
+
except ValueError as e:
|
|
771
|
+
raise HTTPException(status_code=404, detail=str(e))
|
|
772
|
+
except Exception as e:
|
|
773
|
+
logger.error(f"Satellite delivery failed: {e}")
|
|
774
|
+
raise HTTPException(status_code=500, detail=str(e))
|
|
775
|
+
shutil.rmtree(zarr_path, ignore_errors=True)
|
|
776
|
+
ds.to_zarr(zarr_path, mode="w", zarr_format=2)
|
|
777
|
+
import xarray as xr
|
|
778
|
+
|
|
779
|
+
ds = xr.open_zarr(zarr_path)
|
|
780
|
+
valid = ds["value"].notnull()
|
|
781
|
+
return {
|
|
782
|
+
"status": "success",
|
|
783
|
+
"zarr_id": zarr_id,
|
|
784
|
+
"zarr_path": zarr_path,
|
|
785
|
+
"download_url": f"/api/v1/satellite/download/{zarr_id}",
|
|
786
|
+
"products": str(ds.attrs.get("products", "")).split(","),
|
|
787
|
+
"attribution": ds.attrs.get("attribution", ""),
|
|
788
|
+
"valid_share": float(valid.mean()),
|
|
789
|
+
"age_days_median": float(ds["age_days"].where(valid).median())
|
|
790
|
+
if bool(valid.any())
|
|
791
|
+
else None,
|
|
792
|
+
"attrs": json.loads(json.dumps(dict(ds.attrs), default=str)),
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
|
|
796
|
+
@app.get("/api/v1/satellite/download/{zarr_id}")
|
|
797
|
+
def download_satellite(zarr_id: str):
|
|
798
|
+
if not (zarr_id.startswith("sat_") and zarr_id[4:].isalnum()):
|
|
799
|
+
raise HTTPException(status_code=400, detail="invalid store id")
|
|
800
|
+
cache_dir = _satellite_cache_dir()
|
|
801
|
+
zarr_path = os.path.join(cache_dir, f"{zarr_id}.zarr")
|
|
802
|
+
if not os.path.isdir(zarr_path):
|
|
803
|
+
raise HTTPException(status_code=404, detail="Satellite Zarr store not found.")
|
|
804
|
+
zip_path = os.path.join(cache_dir, f"{zarr_id}.zip")
|
|
805
|
+
if _zip_is_stale(zip_path, zarr_path):
|
|
806
|
+
shutil.make_archive(zip_path.replace(".zip", ""), "zip", zarr_path)
|
|
807
|
+
return FileResponse(
|
|
808
|
+
zip_path, media_type="application/zip", filename=f"{zarr_id}.zip"
|
|
809
|
+
)
|
|
810
|
+
|
|
811
|
+
|
|
592
812
|
if __name__ == "__main__":
|
|
593
813
|
import uvicorn
|
|
594
814
|
import argparse
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
{
|
|
2
|
+
"region": "Long Island Sound and its approaches, US Northeast",
|
|
3
|
+
"note": "River mouths only. The gauges and the scale factor of each river are derived for every request from USGS NLDI and site metadata (see forcingkit.fetchers.usgs_rivers). comid is the NHDPlus outlet flowline; lon and lat place the mouth for the bbox test. include and exclude pin or rule out gauges by USGS site number.",
|
|
4
|
+
"mouths": [
|
|
5
|
+
{
|
|
6
|
+
"name": "Bronx",
|
|
7
|
+
"lon": -73.8584,
|
|
8
|
+
"lat": 40.8032,
|
|
9
|
+
"comid": 26431899,
|
|
10
|
+
"note": "Enters the East River at Clason Point. The outlet is set by hand: NHDPlus routes the East River on through New York Harbor."
|
|
11
|
+
},
|
|
12
|
+
{"name": "Housatonic", "lon": -73.1113, "lat": 41.1714, "comid": 7718580},
|
|
13
|
+
{
|
|
14
|
+
"name": "Quinnipiac",
|
|
15
|
+
"lon": -72.9053,
|
|
16
|
+
"lat": 41.2981,
|
|
17
|
+
"comid": 6178614,
|
|
18
|
+
"note": "The outlet basin is New Haven Harbor's, so the scaled flow also stands for the Mill and West Rivers."
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"name": "Connecticut",
|
|
22
|
+
"lon": -72.3305,
|
|
23
|
+
"lat": 41.2726,
|
|
24
|
+
"comid": 7703046,
|
|
25
|
+
"note": "Travel time from Thompsonville to the mouth is roughly a day at high flow; set lag_hours to apply it."
|
|
26
|
+
},
|
|
27
|
+
{"name": "Thames", "lon": -72.0827, "lat": 41.3178, "comid": 6170754}
|
|
28
|
+
]
|
|
29
|
+
}
|